Compartir a través de


Cambios importantes en Roslyn después de las versiones de .NET 9.0.100 a .NET 10.0.100

En este documento se enumeran los cambios disruptivos conocidos en Roslyn después de la versión general de .NET 9 (.NET SDK versión 9.0.100) hasta la versión general de .NET 10 (.NET SDK versión 10.0.100).

scoped en una lista de parámetros lambda ahora siempre es un modificador.

Presentado en Visual Studio 2022 versión 17.13

C# 14 presenta la capacidad de escribir un lambda con modificadores de parámetro, sin tener que especificar un tipo de parámetro: https://github.com/dotnet/csharplang/blob/main/proposals/simple-lambda-parameters-with-modifiers.md

Como parte de este trabajo, se aceptó un cambio disruptivo en el que scoped siempre se tratará como un modificador en un parámetro lambda, incluso cuando hubiera sido aceptado como un nombre de tipo en el pasado. Por ejemplo:

var v = (scoped scoped s) => { ... };

ref struct @scoped { }

En C# 14, se producirá un error, ya que ambos tokens scoped se tratan como modificadores. La solución alternativa consiste en usar @ en la posición de nombre de tipo de la siguiente manera:

var v = (scoped @scoped s) => { ... };

ref struct @scoped { }

Las sobrecargas de Span<T> y ReadOnlySpan<T> se aplican en más casos de C# 14 y versiones más recientes

Presentado en Visual Studio 2022 versión 17.13

C# 14 introduce nuevas conversiones de span incorporadas y reglas de inferencia de tipos. Esto significa que, a diferencia de C# 13, se podrían elegir diferentes sobrecargas, lo que, en algunos casos, podría resultar en un error de compilación por ambigüedad, ya que una nueva sobrecarga es aplicable, pero no hay una única sobrecarga claramente mejor.

En el ejemplo siguiente se señalan algunas ambigüedades y posibles soluciones alternativas. Tenga en cuenta que la otra solución alternativa es que los autores de API usen el OverloadResolutionPriorityAttribute.

var x = new long[] { 1 };
Assert.Equal([2], x); // previously Assert.Equal<T>(T[], T[]), now ambiguous with Assert.Equal<T>(ReadOnlySpan<T>, Span<T>)
Assert.Equal([2], x.AsSpan()); // workaround

var y = new int[] { 1, 2 };
var s = new ArraySegment<int>(y, 1, 1);
Assert.Equal(y, s); // previously Assert.Equal<T>(T, T), now ambiguous with Assert.Equal<T>(Span<T>, Span<T>)
Assert.Equal(y.AsSpan(), s); // workaround

Una sobrecarga Span<T> podría ser seleccionada en C# 14 donde una sobrecarga que utiliza una interfaz implementada por T[] (como IEnumerable<T>) fue elegida en C# 13, lo que puede resultar en un ArrayTypeMismatchException en tiempo de ejecución si se usa con una matriz covariante:

string[] s = new[] { "a" };
object[] o = s; // array variance

C.R(o); // wrote 1 previously, now crashes in Span<T> constructor with ArrayTypeMismatchException
C.R(o.AsEnumerable()); // workaround

static class C
{
    public static void R<T>(IEnumerable<T> e) => Console.Write(1);
    public static void R<T>(Span<T> s) => Console.Write(2);
    // another workaround:
    public static void R<T>(ReadOnlySpan<T> s) => Console.Write(3);
}

Por esa razón, ReadOnlySpan<T> es generalmente preferido sobre Span<T> por resolución de sobrecarga en C# 14. En algunos casos, esto podría provocar interrupciones en la compilación, por ejemplo, cuando hay sobrecargas para Span<T> y ReadOnlySpan<T>, ambas tomando y devolviendo el mismo tipo de segmento:

double[] x = new double[0];
Span<ulong> y = MemoryMarshal.Cast<double, ulong>(x); // previously worked, now compilation error
Span<ulong> z = MemoryMarshal.Cast<double, ulong>(x.AsSpan()); // workaround

static class MemoryMarshal
{
    public static ReadOnlySpan<TTo> Cast<TFrom, TTo>(ReadOnlySpan<TFrom> span) => default;
    public static Span<TTo> Cast<TFrom, TTo>(Span<TFrom> span) => default;
}

Cuando se utiliza C# 14 o posterior y se apunta a un .NET anterior a net10.0 o .NET Framework con referencia System.Memory, hay un cambio de ruptura con y matrices Enumerable.Reverse:

int[] x = new[] { 1, 2, 3 };
var y = x.Reverse(); // previously Enumerable.Reverse, now MemoryExtensions.Reverse

En net10.0 , hay Enumerable.Reverse(this T[]) que tiene precedencia y por lo tanto se evita la ruptura. De lo contrario, MemoryExtensions.Reverse(this Span<T>) se resuelve con una semántica diferente a la de Enumerable.Reverse(this IEnumerable<T>) (que solía resolverse en C# 13 y versiones anteriores). En concreto, la extensión Span realiza la inversión en su lugar y devuelve void. Como método alternativo, puede definir su propio Enumerable.Reverse(this T[]) o usar Enumerable.Reverse explícitamente:

int[] x = new[] { 1, 2, 3 };
var y = Enumerable.Reverse(x); // instead of 'x.Reverse();'

Ahora se informa de los diagnósticos para el método de eliminación basado en patrones en foreach

Presentado en Visual Studio 2022 versión 17.13

Por ejemplo, ahora se notifica un método obsoleto DisposeAsync en await foreach.

await foreach (var i in new C()) { } // 'C.AsyncEnumerator.DisposeAsync()' is obsolete

class C
{
    public AsyncEnumerator GetAsyncEnumerator(System.Threading.CancellationToken token = default)
    {
        throw null;
    }

    public sealed class AsyncEnumerator : System.IAsyncDisposable
    {
        public int Current { get => throw null; }
        public Task<bool> MoveNextAsync() => throw null;

        [System.Obsolete]
        public ValueTask DisposeAsync() => throw null;
    }
}

Establecer el estado del objeto enumerador en "después" durante su eliminación

Presentado en Visual Studio 2022 versión 17.13

La máquina de estados para enumeradores permitía incorrectamente reanudar la ejecución después de que el enumerador fuera eliminado.
Ahora bien, MoveNext() en un enumerador eliminado devuelve correctamente false sin ejecutar más código de usuario.

var enumerator = C.GetEnumerator();

Console.Write(enumerator.MoveNext()); // prints True
Console.Write(enumerator.Current); // prints 1

enumerator.Dispose();

Console.Write(enumerator.MoveNext()); // now prints False

class C
{
    public static IEnumerator<int> GetEnumerator()
    {
        yield return 1;
        Console.Write("not executed after disposal")
        yield return 2;
    }
}

UnscopedRefAttribute no puede utilizarse con reglas de seguridad ref antiguas

Presentado en Visual Studio 2022 versión 17.13

El código compilado por las nuevas versiones del compilador Roslyn UnscopedRefAttribute se veía afectado de forma no intencionada incluso cuando el código se compilaba en el contexto de las reglas de seguridad ref anteriores (es decir, orientado a C# 10 o anterior con net6.0 o anterior). Sin embargo, el atributo no debe tener ningún efecto en ese contexto y ya se ha corregido.

El código que anteriormente no informaba de errores en C# 10 o versiones anteriores con net6.0 o versiones previas ahora podrá no compilarse:

using System.Diagnostics.CodeAnalysis;
struct S
{
    public int F;

    // previously allowed in C# 10 with net6.0
    // now fails with the same error as if the [UnscopedRef] wasn't there:
    // error CS8170: Struct members cannot return 'this' or other instance members by reference
    [UnscopedRef] public ref int Ref() => ref F;
}

Para evitar malentendidos (si tenemos en cuenta que el atributo tiene un efecto, pero realmente no porque el código se compile con las reglas de ref safety anteriores), se genera una advertencia cuando el atributo se usa en C# 10 o versiones anteriores con net6.0 o versiones previas:

using System.Diagnostics.CodeAnalysis;
struct S
{
    // both are errors in C# 10 with net6.0:
    // warning CS9269: UnscopedRefAttribute is only valid in C# 11 or later or when targeting net7.0 or later.
    [UnscopedRef] public ref int Ref() => throw null!;
    public static void M([UnscopedRef] ref int x) { }
}

Microsoft.CodeAnalysis.EmbeddedAttribute se valida en la declaración

Presentado en Visual Studio 2022 versión 17.13

El compilador ahora valida la forma de Microsoft.CodeAnalysis.EmbeddedAttribute cuando se declara en el código fuente. Anteriormente, el compilador permitía declaraciones definidas por el usuario de este atributo, pero solo cuando no era necesario generar una sola. Ahora se valida lo siguiente:

  1. Debe ser interno
  2. Debe ser una clase
  3. Debe estar sellado
  4. Debe ser no estático
  5. Debe tener un constructor interno o público sin parámetros
  6. Debe heredar de System.Attribute.
  7. Debe permitirse en cualquier declaración de tipo (class, struct, interface, enum o delegate).
namespace Microsoft.CodeAnalysis;

// Previously, sometimes allowed. Now, CS9271
public class EmbeddedAttribute : Attribute {}

La expresión field en un descriptor de acceso de propiedad hace referencia al campo de respaldo sintetizado

Presentado en Visual Studio 2022 versión 17.12

La expresión field, cuando se usa dentro de un descriptor de acceso de propiedad, se refiere a un campo de respaldo sintetizado para la propiedad.

La advertencia CS9258 se notifica cuando el identificador se habría vinculado a un símbolo diferente con la versión 13 o anterior del lenguaje.

Para evitar generar un campo de respaldo sintetizado y hacer referencia al miembro existente, use "this.field" o "@field" en su lugar. Como alternativa, cambie el nombre del miembro existente y la referencia a ese miembro para evitar un conflicto con field.

class MyClass
{
    private int field = 0;

    public object Property
    {
        get
        {
            // warning CS9258: The 'field' keyword binds to a synthesized backing field for the property.
            // To avoid generating a synthesized backing field, and to refer to the existing member,
            // use 'this.field' or '@field' instead.
            return field;
        }
    }
}

Variable llamada field no permitida en un descriptor de acceso de propiedad

Presentado en Visual Studio 2022 versión 17.14

La expresión field, cuando se usa dentro de un descriptor de acceso de propiedad, se refiere a un campo de respaldo sintetizado para la propiedad.

El error CS9272 se notifica cuando un parámetro local o de una función anidada con el nombre field se declara en un descriptor de acceso de propiedad.

Para evitar el error, cambie el nombre de la variable o use @field en la declaración.

class MyClass
{
    public object Property
    {
        get
        {
            // error CS9272: 'field' is a keyword within a property accessor.
            // Rename the variable or use the identifier '@field' instead.
            int field = 0;
            return @field;
        }
    }
}

Los tipos record y record struct no pueden definir miembros de tipo puntero, ni siquiera cuando proporcionan sus propias implementaciones de Equals.

Presentado en Visual Studio 2022 versión 17.14

La especificación para los tipos record class y record struct indicaba que cualquier tipo de puntero está prohibido como campo de instancia. Sin embargo, esto no se aplicó correctamente cuando el tipo record class o record struct definía su propia implementación de Equals.

El compilador ahora prohíbe esto correctamente.

unsafe record struct R(
    int* P // Previously fine, now CS8908
)
{
    public bool Equals(R other) => true;
}

La emisión de ejecutables de solo metadatos requiere un punto de entrada

Presentado en Visual Studio 2022 versión 17.14

Anteriormente, el punto de entrada no se establecía intencionadamente cuando se emitían ejecutables en modo de solo metadatos (también conocidos como ensamblados de referencia). Esto ya está corregido, pero también significa que, si falta un punto de entrada, se trata de un error de compilación.

// previously successful, now fails:
CSharpCompilation.Create("test").Emit(new MemoryStream(),
    options: EmitOptions.Default.WithEmitMetadataOnly(true))

CSharpCompilation.Create("test",
    // workaround - mark as DLL instead of EXE (the default):
    options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
    .Emit(new MemoryStream(),
        options: EmitOptions.Default.WithEmitMetadataOnly(true))

Del mismo modo, esto se puede observar cuando se usa el argumento de línea de comandos /refonly o la propiedad ProduceOnlyReferenceAssembly MSBuild.

partial no puede ser un tipo de retorno de un método

Presentado en Visual Studio 2022 versión 17.14

La característica de lenguaje de eventos parciales y compiladores permite el modificador partial en más lugares, por lo que no puede ser un tipo de valor devuelto a menos que se escape:

class C
{
    partial F() => new partial(); // previously worked
    @partial F() => new partial(); // workaround
}

class partial { }