Поделиться через


Критические изменения в Roslyn от .NET 9.0.100 до .NET 10.0.100

В этом документе перечислены известные критические изменения в Roslyn после общего выпуска .NET 9 (пакет SDK для .NET версии 9.0.100) до общего выпуска .NET 10 (пакет SDK для .NET версии 10.0.100).

scoped в списке лямбда-параметров теперь всегда является модификатором.

Введено в Visual Studio 2022 версии 17.13

C# 14 представляет возможность записи лямбда-модуля с модификаторами параметров, не указывая тип параметра: https://github.com/dotnet/csharplang/blob/main/proposals/simple-lambda-parameters-with-modifiers.md

В рамках этой работы было принято ломающее изменение, согласно которому scoped всегда будет выступать в роли модификатора в параметре лямбда, даже если ранее он мог приниматься как имя типа. Например:

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

ref struct @scoped { }

В C# 14 это будет ошибка, так как оба маркера scoped рассматриваются как модификаторы. Обходной путь — использовать @ в позиции имени типа следующим образом:

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

ref struct @scoped { }

Span<T> и ReadOnlySpan<T> перегрузки применимы в большем количестве сценариев в C# 14 и новее

Введено в Visual Studio 2022 версии 17.13

C# 14 содержит новые встроенные преобразования диапазонов и правила вывода типов. Это означает, что различные перегрузки могут быть выбраны по сравнению с C# 13, а иногда может возникнуть неоднозначность во время компиляции, так как новая перегрузка применима, но нет одной лучшей перегрузки.

В следующем примере показаны некоторые неоднозначности и возможные обходные пути. Обратите внимание, что еще одно решение заключается в том, чтобы авторы API использовали 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

В C# 14 может быть выбрана Span<T> перегрузка в тех случаях, когда в C# 13 выбиралась перегрузка, принимающая интерфейс, реализуемый T[] (например, IEnumerable<T>). Это может привести к ArrayTypeMismatchException во время выполнения при использовании с ковариантным массивом.

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);
}

По этой причине ReadOnlySpan<T> обычно предпочтительнее Span<T> в процессе разрешения перегрузок в C# 14. В некоторых случаях это может привести к ошибкам компиляции, например, когда существуют перегрузки для Span<T> и ReadOnlySpan<T>, которые принимают и возвращают один и тот же тип диапазона.

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;
}

При использовании C# 14 или более поздней версии и нацеливания на более старую версию .NET, чем net10.0, или .NET Framework со ссылкой на System.Memory, возникает критическое изменение в отношении Enumerable.Reverse и массивов:

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

На net10.0 есть Enumerable.Reverse(this T[]), которая имеет приоритет, и поэтому удается избежать разрыва. В противном случае MemoryExtensions.Reverse(this Span<T>) интерпретируется с семантикой, отличной от Enumerable.Reverse(this IEnumerable<T>) (которая разрешалась в C# 13 и более ранних версиях). В частности, расширение Span выполняет разворот на месте и возвращает void. В качестве обходного решения можно определить собственные Enumerable.Reverse(this T[]) или использовать Enumerable.Reverse явным образом:

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

Диагностика теперь осуществляется для метода удаления, основанного на шаблонах, в foreach

Введено в Visual Studio 2022 версии 17.13

Например, устаревший метод DisposeAsync теперь сообщается в 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;
    }
}

Задайте для объекта перечислителя значение "after" во время удаления

Введено в Visual Studio 2022 версии 17.13

Компьютер состояния для перечислителей неправильно разрешил повторное выполнение после удаления перечислителя.
Теперь MoveNext() в объекте перечислителя, который уже был удалён, правильно возвращает false без выполнения дополнительного пользовательского кода.

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 нельзя использовать с устаревшими правилами безопасности ссылок

Введено в Visual Studio 2022 версии 17.13

UnscopedRefAttribute непреднамеренно повлиял на код, скомпилированный новыми версиями компилятора Roslyn, даже если этот код был скомпилирован по более ранним правилам безопасности ссылок, то есть для C# 10 или более ранних версий с использованием net6.0 или более ранней. Однако атрибут не должен иметь эффект в этом контексте, и это теперь исправлено.

Код, который ранее не выдавал ошибок в C# 10 или более ранних версиях с net6.0 или более ранними версиями, теперь может не компилироваться.

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;
}

Чтобы предотвратить недоразумения (когда предполагается, что атрибут оказывает эффект, но на самом деле это не так, поскольку код компилируется с использованием более ранних правил безопасного обращения с ссылками), выдается предупреждение при использовании атрибута в C# 10 или более ранних версиях вместе с net6.0 или более ранними версиями.

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 проверяется на этапе декларации

Введено в Visual Studio 2022 версии 17.13

Теперь компилятор проверяет структуру Microsoft.CodeAnalysis.EmbeddedAttribute при объявлении в исходном коде. Ранее компилятор разрешал задаваемые пользователем объявления этого атрибута, но только в тех случаях, когда ему не нужно было генерировать его самостоятельно. Теперь мы проверяем, что:

  1. Он должен быть внутренним
  2. Это должен быть класс
  3. Он должен быть запечатан
  4. Он должен быть нестатичным
  5. Он должен иметь внутренний или общедоступный конструктор без параметров
  6. Он должен наследоваться от System.Attribute.
  7. Это должно быть допустимо в любом объявлении типа (класс, структура, интерфейс, перечисление или делегат)
namespace Microsoft.CodeAnalysis;

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

Выражение field в акцессоре свойства ссылается на синтезированное поле поддержки

Введено в Visual Studio 2022 версии 17.12

Выражение field, когда используется в аксессоре свойства, ссылается на синтезированное поддерживающее поле для свойства.

Предупреждение CS9258 сообщается, когда идентификатор привязан к другому символу с языковой версией 13 или более ранней.

Чтобы избежать создания синтезированного резервного поля, а также для ссылки на существующий член, используйте вместо этого значение "this.field" или "@field". Кроме того, переименуйте существующий элемент и ссылку на него, чтобы избежать конфликта с 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;
        }
    }
}

Переменная с именем field запрещена в аксессоре свойства

введена в Visual Studio 2022 версии 17.14

Выражение field, когда используется в аксессоре свойства, ссылается на автоматически создаваемое вспомогательное поле для свойства.

Ошибка CS9272 сообщается, когда локальная переменная или параметр вложенной функции с именем field объявлены в аксессоре свойства.

Чтобы избежать ошибки, переименуйте переменную или используйте @field в объявлении.

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;
        }
    }
}

Типы record и record struct не могут содержать членов указательного типа, даже если они предоставляют собственные реализации Equals.

введена в Visual Studio 2022 версии 17.14

Спецификация для типов record class и record struct указывает, что любые типы указателей не допускаются в качестве полей экземпляра. Однако это не было применено правильно, когда тип record class или record struct определил собственную реализацию Equals.

Компилятор теперь неправильно запрещает это.

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

Для создания исполняемых файлов, содержащих только метаданные, требуется наличие точки входа

введена в Visual Studio 2022 версии 17.14

Ранее точка входа непреднамеренно оставалась неустановленной при создании исполняемых файлов в режиме только для метаданных (также известных как ссылочные сборки). Это исправлено, но это также означает, что недостающая точка входа является ошибкой компиляции:

// 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))

Аналогично это можно наблюдать при использовании аргумента командной строки /refonly или свойства MSBuild ProduceOnlyReferenceAssembly.

partial не может быть возвращаемым типом методов

введена в Visual Studio 2022 версии 17.14

Возможность языка частичных событий и конструкторов позволяет использовать модификатор partial в большем количестве мест и поэтому не может быть возвращаемым типом, если не будет экранирован:

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

class partial { }

extension рассматривается как контекстное ключевое слово

Представлено в Visual Studio 2022 версии 17.14. Начиная с C# 14 extension ключевое слово служит специальной целью для обозначения контейнеров расширений. Это изменяет способ интерпретации определенных конструкций кода компилятором.

Если вам нужно использовать «расширение» в качестве идентификатора, а не ключевого слова, экранируйте его с помощью префикса @: @extension. Это указывает компилятору рассматривать его как обычный идентификатор вместо ключевого слова.

Компилятор анализирует это как контейнер расширения, а не конструктор.

class @extension
{
    extension(object o) { } // parsed as an extension container
}

Компилятор не сможет проанализировать этот метод как метод с возвращаемым типом extension.

class @extension
{
    extension M() { } // will not compile
}

Представлено в Visual Studio 2022 версии 17.15. Идентификатор расширения может не использоваться в качестве имени типа, поэтому следующее не будет компилироваться:

using extension = ...; // alias may not be named "extension"
class extension { } // type may not be named "extension"
class C<extension> { } // type parameter may not be named "extension"