다음을 통해 공유


.NET 9.0.100부터 .NET 10.0.100까지 Roslyn의 주요 변경 내용

이 문서에서는 .NET 9 일반 릴리스(.NET SDK 버전 9.0.100)부터 .NET 10 일반 릴리스(.NET SDK 버전 10.0.100)까지 Roslyn의 알려진 호환성 손상 변경 사항을 나열합니다.

람다 매개 변수 목록의 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

Span<T> 오버로드는 C# 14에서 선택할 수 있습니다. 여기서 T[] 구현된 인터페이스를 사용하는 오버로드(예: IEnumerable<T>)가 C# 13에서 선택되었으며, 이는 공변 배열과 함께 사용되는 경우 런타임에 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 이상을 사용하고 net10.0 참조를 통해 System.Memory보다 오래된 .NET 또는 .NET Framework를 대상으로 하는 경우, 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>)는 (C# 13 이하에서 해결되던) Enumerable.Reverse(this IEnumerable<T>)과는 다른 의미 체계로 해결됩니다. 특히 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에서 소개

새로운 Roslyn 컴파일러 버전에서 컴파일할 때 UnscopedRefAttribute가 의도치 않게 영향을 미친 코드입니다. 이 코드는 이전의 참조 안전 규칙(즉, net6.0 또는 그 이하의 C# 10 이하를 대상으로)을 준수하여 컴파일되었습니다. 그러나 특성은 해당 컨텍스트에 영향을 미치지 않아야 하며 이제 수정되었습니다.

이전에는 net6.0 이하와 C# 10 이하에서 오류 없이 컴파일되던 코드가 이제는 컴파일에 실패할 수 있습니다.

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 안전 규칙으로 컴파일되었기 때문에 실제로는 그렇지 않음) 특성이 net6.0 이하의 C# 10 이하에서 사용될 때 경고가 보고됩니다.

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는 속성 접근자 내에서 사용할 경우 속성의 생성된 백업 필드를 참조합니다.

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

recordrecord struct 형식은 고유한 Equals 구현을 제공하는 경우에도 포인터 형식 멤버를 정의할 수 없습니다.

Visual Studio 2022 버전 17.14 도입되었습니다.

record classrecord 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 도입되었습니다.

이전에 메타데이터 전용 모드(일명 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))

마찬가지로 명령줄 인수 /refonly 또는 ProduceOnlyReferenceAssembly MSBuild 속성을 사용할 때 이를 관찰할 수 있습니다.