自 .NET 9.0.100 以来到 .NET 10.0.100 的 Roslyn 重大更改

本文件列出了从 .NET 9 常规版本(.NET SDK 版本 9.0.100)到 .NET 10 常规版本(.NET SDK 版本 10.0.100)期间 Roslyn 中已知的重大变更。

lambda 参数列表中的 scoped 现在始终是修饰符。

在 Visual Studio 2022 版本 17.13 中引入

C# 14 引入了使用参数修饰符编写 lambda 的功能,而无需指定参数类型:https://github.com/dotnet/csharplang/blob/main/proposals/simple-lambda-parameters-with-modifiers.md

作为这项工作的一部分,已接受了一个重大更改,其中 scoped 始终被视为 lambda 参数中的修饰符,即使在过去它可能被作为类型名称接受。 例如:

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

因此,在 C# 14 的重载解析中,ReadOnlySpan<T> 通常比 Span<T> 更受青睐。 在某些情况下,这可能会导致编译中断,例如,当 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 不能与旧的 ref 安全性规则一起使用

在 Visual Studio 2022 版本 17.13 中引入

即使代码是在早期的 ref 安全性规则的上下文中编译的(即,针对 C# 10 或更早版本以及 net6.0 或更早版本),UnscopedRefAttribute 也会无意中影响新 Roslyn 编译器版本编译的代码。 然而,该属性在该上下文中不应产生影响,现在已经解决了这个问题。

以前在 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;
}

为了防止误解(认为该属性有效果,但它实际上没有,因为代码是使用早期 ref 安全规则编译的),当属性在 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 在属性访问器中使用时,指的是属性的合成支持字段。

当标识符在语言版本 13 或更早版本中绑定到不同的符号时,会报告警告 CS9258。

若要避免生成合成后盾字段并引用现有成员,请改用“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 在属性访问器中使用时,指的是属性的合成支持字段。

当在属性访问器中声明名称为 field 的局部或嵌套函数参数时,将报告错误 CS9272。

若要避免此错误,请重命名变量,或在声明中使用 @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;
        }
    }
}

即使提供自己的 Equals 实现,recordrecord struct 类型也不能定义指针类型成员

在 Visual Studio 2022 版本 17.14 中引入

record classrecord struct 类型的规范指示不允许任何指针类型作为实例字段。 尽管如此,当 record classrecord struct 类型定义了其自己的 Equals 实现时,这一点并未得到正确执行。

编译器现在正确地禁止这样做。

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

生成仅包含元数据的可执行文件需要一个入口点

在 Visual Studio 2022 版本 17.14 中引入

以前,在仅元数据模式(也称为 ref 程序集)中发出可执行文件时,入口点被无意中取消设置。 这现已更正,但也意味着缺少的入口点是编译错误:

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

同样,在使用命令行参数 /refonlyProduceOnlyReferenceAssembly MSBuild 属性时,可以观察到这种情况。

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”作为标识符而不是关键字,请使用@前缀对其进行转义: @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"

部分属性和事件现在隐式虚拟和公共

Visual Studio 2022 版本 17.15 中引入

我们修复了一个不一致之处,即部分接口属性和事件不会隐式virtualpublic且与非部分等效项不同。 但是,对于分部接口方法 保留这种不一致,以避免发生更大的中断性变更。 请注意,Visual Basic 和其他不支持默认接口成员的语言将开始要求实现隐式虚拟 partial 接口成员。

若要保留以前的行为,请显式标记 partial 接口成员 private (如果他们没有任何辅助功能修饰符),并且 sealed (如果他们没有 private 暗示 sealed的修饰符,并且它们还没有修饰 virtual 符或 sealed)。

System.Console.Write(((I)new C()).P); // wrote 1 previously, writes 2 now

partial interface I
{
    public partial int P { get; }
    public partial int P => 1; // implicitly virtual now
}

class C : I
{
    public int P => 2; // implements I.P
}
System.Console.Write(((I)new C()).P); // inaccessible previously, writes 1 now

partial interface I
{
    partial int P { get; } // implicitly public now
    partial int P => 1;
}

class C : I;