비고
이 문서는 기능 사양입니다. 사양은 기능의 디자인 문서 역할을 합니다. 여기에는 기능 디자인 및 개발 중에 필요한 정보와 함께 제안된 사양 변경 내용이 포함됩니다. 이러한 문서는 제안된 사양 변경이 완료되고 현재 ECMA 사양에 통합될 때까지 게시됩니다.
기능 사양과 완료된 구현 간에 약간의 불일치가 있을 수 있습니다. 이러한 차이는 관련 LDM(언어 디자인 모임) 노트에서 캡처됩니다.
C# 언어 표준에 기능 사양서를 채택하는 프로세스에 대해 더 자세히 알아보려면 사양들 문서를 참조하세요.
챔피언 발행호: https://github.com/dotnet/csharplang/issues/8714
요약
새로운 암시적 변환 형식을 포함하여 언어에 대한 Span<T>
ReadOnlySpan<T>
일류 지원을 도입하고 더 많은 위치에서 고려하여 이러한 정수 계열 형식을 사용하는 보다 자연스러운 프로그래밍을 허용합니다.
동기
C# 7.2에서 도입된 이후, Span<T>
와 ReadOnlySpan<T>
는 여러 중요한 방식으로 언어와 BCL(기본 클래스 라이브러리)에 자리 잡았습니다. 개발자의 안전을 희생하지 않고도 개발자의 소개로 성능이 향상되므로 개발자에게 적합합니다. 그러나 언어는 몇 가지 주요 방법으로 이러한 형식과 거리감을 두어 API의 의도를 명확히 표현하기 어렵게 하여, 새 API에 대해 상당한 양의 반복적인 영역을 초래합니다. 예를 들어, BCL은 .NET 9에 여러 가지 새로운 텐서 기본 API를 추가했지만, 이러한 API는 모두 ReadOnlySpan<T>
에서 제공됩니다. C#은 ReadOnlySpan<T>
, Span<T>
, T[]
간의 관계를 인식하지 못합니다. 따라서 이러한 형식 간에 사용자 정의 변환이 있더라도 확장 메서드 수신기로 사용할 수 없으며, 다른 사용자 정의 변환과 조합할 수 없고, 모든 제네릭 형식 유추 시나리오에 도움이 되지 않습니다.
사용자는 명시적 변환 또는 형식 인수를 사용해야 합니다. 즉, 변환 후 이러한 형식을 전달하는 것이 유효하다는 것을 IDE에 나타내는 것은 없기 때문에 IDE 도구에서 이러한 API를 사용하도록 사용자를 안내하지 않습니다. 이 API 스타일에 최대한의 사용성을 제공하기 위해, BCL은 Span<T>
및 T[]
오버로드의 전체 집합을 정의해야 하며, 이는 실질적인 이익 없이 중복된 표면적을 유지 관리하는 부담입니다. 이 제안은 언어가 이러한 형식과 변환을 보다 직접적으로 인식하도록 하여 문제를 해결하려고 합니다.
예를 들어 BCL은 다음과 같은 도우미의 MemoryExtensions
오버로드를 하나만 추가할 수 있습니다.
int[] arr = [1, 2, 3];
Console.WriteLine(
arr.StartsWith(1) // CS8773 in C# 13, permitted with this proposal
);
public static class MemoryExtensions
{
public static bool StartsWith<T>(this ReadOnlySpan<T> span, T value) where T : IEquatable<T> => span.Length != 0 && EqualityComparer<T>.Default.Equals(span[0], value);
}
이전에는 확장 수신기에 대해 사용자 정의 변환(Span/array/ReadOnlySpan 사이에 있음)이 고려되지 않았기 때문에 Span/array 형식 변수에서 확장 메서드를 사용할 수 있도록 하려면 Span 및 배열 오버로드가 필요했습니다.
상세 디자인
이 제안의 변경 내용은 LangVersion >= 14
에 연결됩니다.
범위 변환
암시적 범위 변환인 §10.2.1의 목록에 새로운 유형의 암시적 변환을 추가합니다. 이 변환은 형식에서의 변환이며 다음과 같이 정의됩니다.
암시적 범위 변환은 array_types
System.Span<T>
System.ReadOnlySpan<T>
다음과 같이 서로 변환할 수 있도록 허용합니다.string
- 요소 형식
array_type
이 있는 모든 1차원Ei
에서System.Span<Ei>
- 요소 형식
array_type
의 모든 1차원Ei
에서System.ReadOnlySpan<Ui>
로,Ei
가 로 공변성 변환 가능(Ui
)한 경우 있습니다. -
System.Span<Ti>
에서System.ReadOnlySpan<Ui>
로, 단Ti
가 로 공변성 변환 가능함을 조건으로 (Ui
) -
System.ReadOnlySpan<Ti>
에서System.ReadOnlySpan<Ui>
로, 단Ti
가 로 공변성 변환 가능함을 조건으로 (Ui
) -
string
부터System.ReadOnlySpan<char>
까지
Span/ReadOnlySpan 형식은 ref struct
인 경우, 정규화된 이름(LDM 2024-06-24)과 일치하면 변환에 적용 가능한 것으로 간주됩니다.
또한 암시적 범위 변환 을 표준 암시적 변환 목록(§10.4.2)에 추가합니다. 이를 통해 오버로드 해석은 이전에 연결된 API 제안에서처럼 인수 해결을 수행할 때 이를 고려할 수 있습니다.
명시적 범위 변환은 다음과 같습니다.
- 모든 암시적 범위 변환.
-
array_type에서
Ti
형식의 요소를System.Span<Ui>
또는System.ReadOnlySpan<Ui>
로 변환할 수 있는 경우, 명시적 참조 변환이Ti
에서Ui
로 존재해야 합니다.
다른 표준 명시적 변환 (§10.4.3)과 달리 표준 명시적 범위 변환은 항상 반대되는 표준 암시적 변환을 감안할 때 존재하지 않습니다.
사용자 정의 변환
암시적 또는 명시적 범위 변환이 존재하는 형식 간에 변환할 때 사용자 정의 변환은 고려되지 않습니다.
암시적 범위 변환은 사용자 정의가 아닌 변환이 존재하는 형식 간에 사용자 정의 연산자를 정의할 수 없다는 규칙에서 제외됩니다(§10.5.2 허용되는 사용자 정의 변환). 이는 BCL이 C# 14로 전환하는 경우에도 기존 Span 변환 연산자를 계속 정의할 수 있도록 필요합니다(이러한 연산자는 여전히 낮은 LangVersions에 필요하며 새 표준 범위 변환의 codegen에서 사용되기 때문). 그러나 구현 세부 정보(codegen 및 lower LangVersions는 사양의 일부가 아님)로 볼 수 있으며 Roslyn은 어쨌든 사양의 이 부분을 위반합니다(사용자 정의 변환에 대한 이 특정 규칙은 적용되지 않음).
확장 수신기
또한 적용 가능성(12.8.9.3)(굵게 변경)을 결정할 때 확장 메서드의 첫 번째 매개 변수에 허용되는 암시적 변환 목록에 암시적 범위 변환을 추가합니다.
다음과 같은 경우 확장 메서드
Cᵢ.Mₑ
를 사용할 수 있습니다 .
Cᵢ
는 일반적이지 않고 중첩되지 않은 클래스입니다.Mₑ
의 이름은 식별자입니다.Mₑ
액세스 가능하며 위에서 설명한 대로 인수를 정적 메서드로 적용할 때 적용할 수 있습니다.- 암시적 동일성, 참조
또는 박싱, 박싱 또는 범위 변환이 expr에서 첫 번째Mₑ
매개 변수의 형식으로 존재합니다. 메서드 그룹 변환에 대해 오버로드 확인이 수행될 때는 범위 변환이 고려되지 않습니다.
메서드 그룹 변환(LDM 2024-07-15)의 확장 수신기에는 암시적 범위 변환이 고려되지 않으므로 컴파일 시간 오류가 CS1113: Extension method 'E.M<int>(Span<int>, int)' defined on value type 'Span<int>' cannot be used to create delegates
발생하지 않고 다음 코드가 계속 작동합니다.
using System;
using System.Collections.Generic;
Action<int> a = new int[0].M; // binds to M<int>(IEnumerable<int>, int)
static class E
{
public static void M<T>(this Span<T> s, T x) => Console.Write(1);
public static void M<T>(this IEnumerable<T> e, T x) => Console.Write(2);
}
가능한 향후 작업에서는 메서드 그룹 변환에서 확장 수신기에 대해 범위 변환이 고려되지 않는 이 조건을 제거하고 대신 위의 시나리오와 같은 시나리오에서 오버로드를 성공적으로 호출 Span
할 수 있도록 변경 내용을 구현하는 것이 좋습니다.
- 컴파일러는 배열을 수신기로 받아 내부에서 범위 변환을 수행하도록 설정된 thunk를 생성할 수 있습니다. 사용자가 직접 대리자를 만드는 것과 비슷한 방식입니다
x => new int[0].M(x)
. - 구현된 경우 값 대리자는 수신기로
Span
를 직접 받을 수 있습니다.
분산
암시적 범위 변환에서 분산 섹션의 목표는 에 대한 약간의 공변도를 복제하는 것입니다System.ReadOnlySpan<T>
. 여기서 제네릭을 통해 분산을 완전히 구현하려면 런타임 변경이 필요하지만(제네릭의 형식 사용 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-13.0/ref-struct-interfaces.md 참조ref struct
) 제안된 .NET 9 APIhttps://github.com/dotnet/runtime/issues/96952를 사용하여 제한된 양의 공변을 허용할 수 있습니다. 언어가 특정 시나리오에서 System.ReadOnlySpan<T>
이 T
로 선언된 것처럼 out T
를 처리할 수 있습니다. 그러나 모든 분산 시나리오를 통해 이 변형 변환을 배관하지 않으며 § 18.2.3.3에서 분산 변환의 정의에 추가하지 않습니다. 만약 나중에 런타임을 변경하여 이 부분의 변동성을 더 깊이 이해하게 된다면, 사소한 호환성 문제를 감수하여 언어에서 이를 완전히 인식할 수 있도록 할 수 있습니다.
패턴
모든 패턴에서 형식으로 사용되는 경우 ref struct
는 오직 정체성 변환만 허용됩니다.
class C<T> where T : allows ref struct
{
void M1(T t) { if (t is T x) { } } // ok (T is T)
void M2(R r) { if (r is R x) { } } // ok (R is R)
void M3(T t) { if (t is R x) { } } // error (T is R)
void M4(R r) { if (r is T x) { } } // error (R is T)
}
ref struct R { }
is-type 연산자의 사양에서(§12.12.12.1):
작업의
E is T
결과는E
이 null이 아니며 참조 변환, 박싱 변환, 언박싱 변환, 래핑 변환 또는 래핑 해제 변환을 통해T
형식으로 성공적으로 변환할 수 있는지 여부를 나타내는 부울 값입니다.[...]
T
이 nullable이 아닌 값 형식인 경우,true
와D
이 같은 형식이면 결과는T
입니다.
이 동작은 이 기능으로 변경되지 않으므로 Span
/ReadOnlySpan
에 대해서는 패턴을 작성할 수 없습니다. 다만, 배열(분산 포함)에 대해서는 유사한 패턴이 가능합니다.
using System;
M1<object[]>(["0"]); // prints
M1<string[]>(["1"]); // prints
void M1<T>(T t)
{
if (t is object[] r) Console.WriteLine(r[0]); // ok
}
void M2<T>(T t) where T : allows ref struct
{
if (t is ReadOnlySpan<object> r) Console.WriteLine(r[0]); // error
}
코드 생성
변환은 구현하는 데 사용된 런타임 도우미가 있는지 여부에 관계없이 항상 존재합니다(LDM 2024-05-13). 도우미가 없는 경우 변환을 사용하려고 하면 컴파일러 필수 멤버가 누락된 컴파일 시간 오류가 발생합니다.
컴파일러는 다음 도우미 또는 동등한 도우미를 사용하여 변환을 구현해야 합니다.
변환 | 도우미 |
---|---|
배열을 Span으로 변환하기 |
static implicit operator Span<T>(T[]) (Span<T> 에 정의됨) |
배열을 ReadOnlySpan으로 변환 |
static implicit operator ReadOnlySpan<T>(T[]) (ReadOnlySpan<T> 에 정의됨) |
스팬에서 읽기 전용 스팬으로 |
static implicit operator ReadOnlySpan<T>(Span<T>) (Span<T> 에 정의된) 및 static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>) |
ReadOnlySpan에서 ReadOnlySpan으로 | static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>) |
String에서 ReadOnlySpan으로 | static ReadOnlySpan<char> MemoryExtensions.AsSpan(string) |
MemoryExtensions.AsSpan
에 정의된 동일한 암시적 연산자 대신 string
가 사용됩니다.
즉, codegen은 LangVersions 간에 다릅니다(암시적 연산자는 C# 13에서 사용되고 정적 메서드 AsSpan
는 C# 14에서 사용됨).
반면에 변환은 .NET Framework에서 내보낼 수 있습니다(메서드가 AsSpan
있는 반면 연산자는 string
그렇지 않음).
(ReadOnly)Span 변환에 대한 명시적 배열은 먼저 원본 배열에서 대상 요소 형식의 배열로 명시적으로 변환한 다음 암시적 변환과 동일한 도우미를 통해 (ReadOnly)Span으로 변환합니다( 즉, 해당 op_Implicit(T[])
).
표현식에서 더 나은 변환
암시적 범위 변환을 선호하도록 식에서 더 나은 변환(§12.6.4.5)이 업데이트되었습니다. 이는 컬렉션 식 오버로드 해결 변경 내용을 기반으로 합니다.
주어진 식
C₁
을(를) 형식E
으로 변환하는 암시적 변환T₁
과(와), 식C₂
을(를) 형식E
으로 변환하는 암시적 변환T₂
이 있을 때, 다음 조건 중 하나가 충족되는 경우C₁
은(는) 보다C₂
입니다.
E
는 컬렉션 식이며,C₁
는 보다 더 나은 입니다.E
가 컬렉션 식 이 아니며 다음 중 하나가 유지됩니다.
E
정확히T₁
일치하며E
T₂
정확히 일치하지 않습니다.E
는T₁
와T₂
어느 쪽과도 정확히 일치하지 않으며,C₁
는 암시적 범위 변환이고,C₂
는 암시적 범위 변환이 아닙니다.E
는T₁
및T₂
와 정확히 일치하거나 전혀 일치하지 않으며,C₁
및C₂
는 암시적 범위 변환입니다, 그리고T₁
가 보다T₂
입니다.E
는 메서드 그룹이며 변환T₁
을 위해 메서드 그룹의 단일 최상의 메서드와 호환되며C₁
변환T₂
을 위해 메서드 그룹의 단일 최상의 메서드와 호환되지 않습니다.C₂
더 나은 변환 대상
더 나은 변환 대상 (§12.6.4.7)이 ReadOnlySpan<T>
보다 Span<T>
를 선호하도록 업데이트되었습니다.
두 가지 형식
T₁
및T₂
이 주어질 때, 만약 다음 중 하나가 성립하면T₁
는 보다 더 나은 변환 대상 입니다.
T₁
는System.ReadOnlySpan<E₁>
,T₂
는System.Span<E₂>
이며,E₁
에서E₂
로 ID 변환이 존재합니다.T₁
는System.ReadOnlySpan<E₁>
,T₂
는System.ReadOnlySpan<E₂>
이며T₁
에서T₂
로의 암시적 변환은 존재하지만T₂
에서T₁
로의 암시적 변환은 존재하지 않습니다T₁
또는T₂
중 적어도 하나는System.ReadOnlySpan<Eᵢ>
및System.Span<Eᵢ>
가 아니며,T₁
에서T₂
로의 암시적 변환은 존재하지만,T₂
에서T₁
로의 암시적 변환은 존재하지 않습니다.- ...
디자인 회의:
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-04.md#preferring-readonlyspant-over-spant-conversions
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-09.md#first-class-span-open-questions
개선된 의견
식에서 더 나은 변환을 위한 규칙은 새 스팬 변환으로 인해 오버로드가 적용될 때마다 새로 적용되는 오버로드가 우선적으로 선택되어 다른 오버로드와의 잠재적 모호성이 발생하지 않도록 해야 합니다.
이 규칙이 없으면 확장 메서드 수신기에 적용할 수 있는 배열에서 ReadOnlySpan으로의 새로운 표준 암시적 변환으로 인해 C# 13에서 성공적으로 컴파일된 다음 코드로 인해 C# 14에서 모호성 오류가 발생합니다.
using System;
using System.Collections.Generic;
var a = new int[] { 1, 2, 3 };
a.M();
static class E
{
public static void M(this IEnumerable<int> x) { }
public static void M(this ReadOnlySpan<int> x) { }
}
또한 이 규칙을 사용하면 이전에 모호성을 유발하는 새 API를 도입할 수 있습니다. 예를 들면 다음과 같습니다.
using System;
using System.Collections.Generic;
C.M(new int[] { 1, 2, 3 }); // would be ambiguous before
static class C
{
public static void M(IEnumerable<int> x) { }
public static void M(ReadOnlySpan<int> x) { } // can be added now
}
경고
“우월성 규칙”은 LangVersion >= 14
에만 존재하는 범위 변환에 대해 정의되므로, API 작성자가 계속해서 LangVersion <= 13
사용자 지원을 유지하려는 경우, 이러한 새로운 오버로드를 추가할 수 없습니다.
예를 들어 .NET 9 BCL에서 이러한 오버로드를 도입하는 경우, net9.0
TFM으로 업그레이드하지만 더 낮은 LangVersion을 유지하는 사용자는 기존 코드에 모호성 오류가 발생할 수 있습니다.
아래 의 공개 질문 도 참조하세요.
형식 유추
사양의 형식 추론 섹션을 다음과 같이 업데이트합니다(굵게 변경).
12.6.3.9 정확한 유추
형식에서 형식으로
U
를 다음과 같이 수행합니다.
- 고정되지 않은
중 하나인 경우 가 에 대한 정확한 범위 집합에 추가됩니다 . - 그렇지 않으면,
V₁...Vₑ
집합과U₁...Uₑ
집합은 다음 경우 중 하나가 적용되는지를 확인하여 결정됩니다.
V
V₁[...]
배열 형식이며U
동일한 순위의U₁[...]
배열 형식입니다.V
는Span<V₁>
이며U
는 배열 형식U₁[]
또는Span<U₁>
V
는ReadOnlySpan<V₁>
와 배열U
형식U₁[]
또는Span<U₁>
또는ReadOnlySpan<U₁>
V
는V₁?
형식이고U
는U₁
형식입니다.V
는 생성된 형식C<V₁...Vₑ>
이고U
는 생성된 형식C<U₁...Uₑ>
입니다.
이러한 사례가 적용되는 경우 각각 에서 해당하는Uᵢ
로Vᵢ
가 이루어집니다.- 그렇지 않으면 유추가 이루어지지 않습니다.
12.6.3.10 하한 추론
형식
에서 형식 로하한 유추는 다음과 같이 이루어집니다.
V
가 고정되지 않은Xᵢ
중 하나이면,U
가Xᵢ
의 하한 집합에 추가됩니다.- 그렇지 않으면
V
이(가)V₁?
타입이고U
이(가)U₁?
타입이라면,U₁
에서V₁
로 하한 유추가 이루어집니다.- 그렇지 않으면,
U₁...Uₑ
집합과V₁...Vₑ
집합은 다음 경우 중 하나가 적용되는지를 확인하여 결정됩니다.
V
는 배열 형식V₁[...]
이며U
동일한 순위의 배열 형식U₁[...]
입니다.V
는Span<V₁>
이며U
는 배열 형식U₁[]
또는Span<U₁>
V
는ReadOnlySpan<V₁>
와 배열U
형식U₁[]
또는Span<U₁>
또는ReadOnlySpan<U₁>
V
는IEnumerable<V₁>
,ICollection<V₁>
,IReadOnlyList<V₁>>
,IReadOnlyCollection<V₁>
또는IList<V₁>
중 하나이며,U
는 단일 차원 배열 형식인U₁[]
입니다.V
은(는) 생성된class
,struct
,interface
또는delegate
유형C<V₁...Vₑ>
이며, 고유한 유형인C<U₁...Uₑ>
이(가) 존재하여U
(또는U
이(가)parameter
유형인 경우, 해당 유효 기본 클래스 또는 유효한 인터페이스 집합의 멤버)이inherits
와(과) 동일하거나 (직접 또는 간접적으로) 상속하거나 (직접 또는 간접적으로)C<U₁...Uₑ>
을(를) 구현합니다.- "‘고유성’ 제한은 인터페이스
C<T>{} class U: C<X>, C<Y>{}
의 경우에 적용되며, 이때U
이C<T>
또는U₁
일 수 있기 때문에X
에서Y
로 유추할 때 유추가 이루어지지 않는다는 것을 의미합니다."
이러한 사례가 적용되는 경우, 다음과 같이 각Uᵢ
부터 해당Vᵢ
로 대한 유추가 수행됩니다.Uᵢ
이(가) 참조 유형으로 알려지지 않은 경우, 정확한 추론이 이루어집니다.- 그렇지 않으면,
이 배열 형식이면 하한 유추 가 수행되고,의 유형에 따라 유추 결과가 달라집니다.
- 만약
V
이Span<Vᵢ>
이라면, 정확한 유추가 이루어집니다.V
이 배열 형식이거나ReadOnlySpan<Vᵢ>
인 경우, 하한 유추가 수행됩니다.- 그렇지 않으면,
U
이(가)Span<Uᵢ>
인 경우, 유추는V
의 유형에 따라 달라집니다.
- 만약
V
이Span<Vᵢ>
이라면, 정확한 유추가 이루어집니다.- 만약
V
가ReadOnlySpan<Vᵢ>
이라면, 하한 유추가 이루어집니다.- 그렇지 않으면,
U
이ReadOnlySpan<Uᵢ>
이고V
이ReadOnlySpan<Vᵢ>
이면, 하한 유추가 이루어집니다.- 그렇지 않고,
V
이C<V₁...Vₑ>
인 경우 유추는i-th
의C
유형 매개 변수에 따라 달라집니다.
- 공변성인 경우 하한 유추 가 이루어집니다.
- 반공변성인 경우 상한 유추 가 이루어집니다.
- 고정된 경우 정확한 유추 가 이루어집니다.
- 그렇지 않으면 유추가 이루어지지 않습니다.
상한 유추에 대한 규칙은 적중할 수 없기 때문입니다.
형식 유추는 상한으로 시작되지 않으며 하한 유추 및 반공변 형식 매개 변수를 통과해야 합니다.
"참조 형식으로 알려져 있지 않으면 Uᵢ
가 이루어진다는 규칙 때문에, Span
/는 (참조 형식일 수 없으므로) 원본 형식 인수로 사용할 수 없습니다."
그러나 상한 범위 유추는 다음과 같은 규칙이 있기 때문에 원본 형식이 있는 Span
/ReadOnlySpan
경우에만 적용됩니다.
U
는Span<U₁>
이며V
는 배열 형식V₁[]
또는Span<V₁>
U
는ReadOnlySpan<U₁>
와 배열V
형식V₁[]
또는Span<V₁>
또는ReadOnlySpan<V₁>
파괴적 변경
기존 시나리오의 변환을 변경하는 제안과 마찬가지로 이 제안에서는 새로운 주요 변경 내용을 도입합니다. 다음은 몇 가지 예입니다.
Reverse
배열에서 호출하기
x.Reverse()
형식의 인스턴스인 x
을(를) 호출하면, 이전에는 T[]
에 바인딩되었지만, 지금은 IEnumerable<T> Enumerable.Reverse<T>(this IEnumerable<T>)
에 바인딩됩니다.
아쉽게도 이러한 API는 호환되지 않습니다(후자는 현재 위치에서 반전을 수행하고 반환 void
).
.NET 10은 배열별 오버로드IEnumerable<T> Reverse<T>(this T[])
https://github.com/dotnet/runtime/issues/107723를 추가하여 이를 완화합니다.
void M(int[] a)
{
foreach (var x in a.Reverse()) { } // fine previously, an error now (`Reverse` returns `void`)
foreach (var x in Enumerable.Reverse(a)) { } // workaround
}
참고 항목:
- https://developercommunity.visualstudio.com/t/Extension-method-SystemLinqEnumerable/10790323
- https://developercommunity.visualstudio.com/t/Compilation-Error-When-Calling-Reverse/10818048
- https://developercommunity.visualstudio.com/t/Version-17131-has-an-obvious-defect-th/10858254
- https://developercommunity.visualstudio.com/t/Visual-Studio-2022-update-breaks-build-w/10856758
- https://github.com/dotnet/runtime/issues/111532
- https://developercommunity.visualstudio.com/t/Backward-compatibility-issue-:-IEnumerab/10896189#T-ND10896782
디자인 모임: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#reverse
모호성
다음 예제에서는 이전에 Span 오버로드에 대한 형식 유추에 실패했지만 이제는 배열에서 Span으로의 형식 유추가 성공하므로 모호합니다.
이 해결을 위해 사용자가 사용 .AsSpan()
하거나 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 x = new int[] { 1, 2 };
var s = new ArraySegment<int>(x, 1, 1);
Assert.Equal(x, s); // previously Assert.Equal<T>(T, T), now ambiguous with Assert.Equal<T>(Span<T>, Span<T>)
Assert.Equal(x.AsSpan(), s); // workaround
xUnit은 다음 https://github.com/xunit/xunit/discussions/3021을 완화하기 위해 더 많은 오버로드를 추가합니다.
디자인 모임: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#new-ambiguities
공변 배열
오버로드에서 IEnumerable<T>
는 공변 배열에서 작동했지만, 현재 권장되는 Span<T>
를 사용하는 오버로드는 그렇지 않습니다. 이는 범위 변환이 공변 배열에 대해 ArrayTypeMismatchException
를 발생시키기 때문입니다.
논란의 여지는 있지만, Span<T>
오버로드는 존재하지 않아야 하며, 대신 ReadOnlySpan<T>
를 사용해야 합니다.
이 문제를 해결하기 위해 사용자는 .AsEnumerable()
을(를) 사용할 수 있습니다. 또한, API 작성자는 OverloadResolutionPriorityAttribute
을(를) 사용하거나, ReadOnlySpan<T>
에 따라 선호되는 오버로드인 을(를) 추가할 수 있습니다.
string[] s = new[] { "a" };
object[] o = s;
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);
}
디자인 모임: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#covariant-arrays
ReadOnlySpan을 Span보다 선호하기
우선 규칙은 ArrayTypeMismatchException
에서 을 방지하기 위해 Span 오버로드보다 ReadOnlySpan 오버로드를 선호하도록 합니다.
예를 들어 오버로드가 반환 형식과 다른 경우와 같은 일부 시나리오에서 컴파일 중단이 발생할 수 있습니다.
double[] x = new double[0];
Span<ulong> y = MemoryMarshal.Cast<double, ulong>(x); // previously worked, now a compilation error (returns ReadOnlySpan, not Span)
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;
}
https://github.com/dotnet/roslyn/issues/76443을(를) 참조하세요.
표현식 트리
스팬과 같은 MemoryExtensions.Contains
를 사용하는 오버로드는 식 트리 내에서도 클래식 오버로드 Enumerable.Contains
보다 선호됩니다. 그러나 ref 구조체는 인터프리터 엔진에서 지원되지 않습니다.
Expression<Func<int[], int, bool>> exp = (array, num) => array.Contains(num);
exp.Compile(preferInterpretation: true); // fails at runtime in C# 14
Expression<Func<int[], int, bool>> exp2 = (array, num) => Enumerable.Contains(array, num); // workaround
exp2.Compile(preferInterpretation: true); // ok
마찬가지로, LINQ-to-SQL과 같은 번역 엔진은 트리 방문자가 Enumerable.Contains
를 예상하나 실제로는 MemoryExtensions.Contains
에 직면하게 되는 상황에서 이에 대응해야 합니다.
참고 항목:
- https://github.com/dotnet/runtime/issues/109757
- https://github.com/dotnet/docs/issues/43952
- https://github.com/dotnet/efcore/issues/35100
- https://github.com/dotnet/csharplang/discussions/8959
디자인 회의:
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-04.md#conversions-in-expression-trees
- https://github.com/dotnet/csharplang/blob/main/meetings/2025/LDM-2025-01-06.md#ignoring-ref-structs-in-expressions
상속을 통한 사용자 정의 변환
암시적 범위 변환을 표준 암시적 변환 목록에 추가하면 사용자 정의 변환이 형식 계층 구조에 관련될 때 동작을 변경할 수 있습니다. 이 예제에서는 새 C# 14 동작으로 이미 동작하는 정수 시나리오와 비교하여 변경 사항을 보여 줍니다.
Span<string> span = [];
var d = new Derived();
d.M(span); // Base today, Derived tomorrow
int i = 1;
d.M(i); // Derived today, demonstrates new behavior
class Base
{
public void M(Span<string> s)
{
Console.WriteLine("Base");
}
public void M(int i)
{
Console.WriteLine("Base");
}
}
class Derived : Base
{
public static implicit operator Derived(ReadOnlySpan<string> r) => new Derived();
public static implicit operator Derived(long l) => new Derived();
public void M(Derived s)
{
Console.WriteLine("Derived");
}
}
https://github.com/dotnet/roslyn/issues/78314도 참조하세요.
확장 메서드 조회
확장 메서드 조회에서 암시적 범위 변환을 허용하면 오버로드 확인으로 해결되는 확장 메서드를 변경할 수 있습니다.
namespace N1
{
using N2;
public class C
{
public static void M()
{
Span<string> span = new string[0];
span.Test(); // Prints N2 today, N1 tomorrow
}
}
public static class N1Ext
{
public static void Test(this ReadOnlySpan<string> span)
{
Console.WriteLine("N1");
}
}
}
namespace N2
{
public static class N2Ext
{
public static void Test(this Span<string> span)
{
Console.WriteLine("N2");
}
}
}
질문 열기
무제한 우수성 규칙
우리는 LangVersion에 대해 향상 규칙을 무조건 적용해야 하는가? 이렇게 하면 API 작성자가 이전 LangVersions 또는 다른 컴파일러 또는 언어(예: VB)에서 사용자를 중단하지 않고 IEnumerable 동등한 항목이 존재하는 새 Span API를 추가할 수 있습니다. 그러나 이는 사용자가 도구 집합을 업데이트한 후(LangVersion 또는 TargetFramework를 변경하지 않고) 다른 동작을 얻을 수 있음을 의미합니다.
- 컴파일러는 다른 오버로드를 선택할 수 있습니다(기술적으로 호환성이 손상되는 변경이지만 해당 오버로드에 동일한 동작이 있기를 바랍니다).
- 다른 휴식이 발생할 수 있지만, 현재로서는 알 수 없습니다.
OverloadResolutionPriorityAttribute
이전 LangVersions에서도 무시되므로 이 문제를 완전히 해결할 수 없습니다.
그러나 특성을 인식해야 하는 VB의 모호성을 방지하기 위해 사용할 수 있어야 합니다.
더 많은 사용자 정의 변환 무시
언어 정의 암시적 및 명시적 범위 변환이 있는 형식 쌍 집합을 정의했습니다.
언어 정의 범위 변환이 T1
에서 T2
으로 존재할 때마다, T1
에서 T2
으로의 사용자 정의 변환은 암시적이든 명시적이든 관계없이 무시됩니다.
여기에는 모든 조건이 포함되므로 예를 들어 범위 변환이 없는 경우(범위 변환 Span<object>
ReadOnlySpan<string>
Span<T>
ReadOnlySpan<U>
이 있지만 이를 T : U
유지해야 함) 사용자 정의 변환이 있는 경우 해당 형식 간에 고려됩니다(변환 연산자는 제네릭 매개 변수를 가질 수 없기 때문에 특수 변환 Span<T>
ReadOnlySpan<string>
이어야 함).
해당 언어 정의 범위 변환이 없는 배열/Span/ReadOnlySpan/문자열 형식의 다른 조합 간에도 사용자 정의 변환을 무시해야 하나요?
예를 들어 사용자 정의 변환이 있는 ReadOnlySpan<T>
Span<T>
경우 무시해야 하나요?
고려할 사양 가능성:
-
, .에서 로의 에서 로의 사용자 정의 변환을 무시합니다 -
사용자 정의 변환은 변환할 때 고려되지 않습니다.
- 1차원
array_type
및System.Span<T>
/System.ReadOnlySpan<T>
- 어떠한
System.Span<T>
/System.ReadOnlySpan<T>
의 조합 -
string
및System.ReadOnlySpan<char>
.
- 1차원
- 위와 마찬가지로 마지막 글머리 기호를 다음으로 바꿉다.
-
string
과System.Span<char>
/System.ReadOnlySpan<char>
.
-
- 위와 마찬가지로 마지막 글머리 기호를 다음으로 바꿉다.
-
string
과System.Span<T>
/System.ReadOnlySpan<T>
.
-
기술적으로 사양은 이러한 사용자 정의 변환 중 일부를 정의할 수 없습니다. 사용자 정의가 아닌 변환이 존재하는 형식 간에 사용자 정의 연산자를 정의할 수 없습니다(§10.5.2).
그러나 Roslyn은 의도적으로 명세서의 이 부분을 위반합니다. 그럼에도 불구하고 Span
와 string
사이의 일부 변환은 허용되며, 이러한 형식 간에는 언어로 정의된 변환이 존재하지 않습니다.
그럼에도 불구하고, 변환을 단순히 무시하는 대신, 아예 정의할 수 없도록 금지하고, 적어도 이러한 새로운 범위 변환에 대해서는 사양 위반 문제를 해결하는 방법을 모색할 수 있습니다. 즉, Roslyn이 이러한 변환이 정의된 경우 실제로 컴파일 시간 오류를 보고하도록 수정합니다(이미 BCL에 의해 정의된 변환은 제외 가능성이 높습니다).
대안
그대로 유지합니다.
C# feature specifications