범위
메모
이 문서는 기능 사양입니다. 사양은 기능의 디자인 문서 역할을 합니다. 여기에는 기능 디자인 및 개발 중에 필요한 정보와 함께 제안된 사양 변경 내용이 포함됩니다. 이러한 문서는 제안된 사양 변경이 완료되고 현재 ECMA 사양에 통합될 때까지 게시됩니다.
기능 사양과 완료된 구현 간에 약간의 불일치가 있을 수 있습니다. 이러한 차이는관련
사양문서에서 스펙릿을 C# 언어 표준으로 채택하는 과정에 대해 자세히 알아볼 수 있습니다.
챔피언 이슈: https://github.com/dotnet/csharplang/issues/185
요약
이 기능은 System.Index
및 System.Range
개체를 생성하고 런타임에 컬렉션을 인덱싱/조각화하는 데 사용할 수 있는 두 개의 새 연산자를 제공하는 것입니다.
개요
잘 알려진 형식 및 멤버
System.Index
및 System.Range
새 구문 양식을 사용하려면 사용되는 구문 폼에 따라 잘 알려진 새로운 형식과 멤버가 필요할 수 있습니다.
"hat" 연산자(^
)를 사용하려면 다음이 필요합니다.
namespace System
{
public readonly struct Index
{
public Index(int value, bool fromEnd);
}
}
배열 요소 액세스에서 System.Index
형식을 인수로 사용하려면 다음 멤버가 필요합니다.
int System.Index.GetOffset(int length);
..
System.Range
구문에는 System.Range
형식과 다음 멤버 중 하나 이상이 필요합니다.
namespace System
{
public readonly struct Range
{
public Range(System.Index start, System.Index end);
public static Range StartAt(System.Index start);
public static Range EndAt(System.Index end);
public static Range All { get; }
}
}
..
구문은 해당 인수가 하나도 없거나, 하나만 없거나, 둘 다 없을 수 있습니다. 인수 수에 관계없이 Range
생성자는 항상 Range
구문을 사용하기에 충분합니다. 그러나 다른 멤버가 있고 하나 이상의 ..
인수가 누락된 경우 해당 멤버가 대체될 수 있습니다.
마지막으로 배열 요소 액세스 식에서 System.Range
형식의 값을 사용하려면 다음 멤버가 있어야 합니다.
namespace System.Runtime.CompilerServices
{
public static class RuntimeHelpers
{
public static T[] GetSubArray<T>(T[] array, System.Range range);
}
}
System.Index
C#은 처음부터 컬렉션을 인덱싱할 방법이 없지만 대부분의 인덱서는 "처음부터" 개념을 사용하거나 "length - i" 식을 수행합니다. "끝에서"를 의미하는 새로운 Index 표현을 소개합니다. 이 기능은 새로운 단항 접두사 "hat" 연산자를 도입합니다. 해당 단일 피연산자는 System.Int32
로 변환 가능해야 합니다. 적절한 System.Index
팩터리 메서드 호출로 낮아지게 됩니다.
다음과 같은 추가 구문 형식으로 unary_expression 문법을 보강합니다.
unary_expression
: '^' unary_expression
;
이를 끝 연산자의
System.Index operator ^(int fromEnd);
이 연산자의 동작은 0보다 크거나 같은 입력 값에 대해서만 정의됩니다.
예제:
var array = new int[] { 1, 2, 3, 4, 5 };
var thirdItem = array[2]; // array[2]
var lastItem = array[^1]; // array[new Index(1, fromEnd: true)]
System.Range
C#에는 컬렉션의 "범위" 또는 "조각"에 액세스하는 구문 방법이 없습니다. 일반적으로 사용자는 메모리 조각에서 필터링/작동하거나 list.Skip(5).Take(2)
같은 LINQ 메서드에 의존하기 위해 복잡한 구조를 구현해야 합니다.
System.Span<T>
및 기타 유사한 형식이 추가됨에 따라 이러한 종류의 작업을 언어/런타임의 더 깊은 수준에서 지원하고 인터페이스를 통합하는 것이 더 중요해집니다.
언어는 새 범위 연산자 x..y
도입합니다. 두 식을 허용하는 이진 접두사 연산자입니다. 피연산자 중 하나를 생략할 수 있으며 (아래 예제 참조) System.Index
로 변환할 수 있어야 합니다. 적절한 System.Range
팩터리 메서드를 호출하도록 낮아질 것입니다.
새 우선 순위 수준을 도입하기 위해 multiplicative_expression에 대한 C# 문법 규칙을 다음과 같이 교체합니다.
range_expression
: unary_expression
| range_expression? '..' range_expression?
;
multiplicative_expression
: range_expression
| multiplicative_expression '*' range_expression
| multiplicative_expression '/' range_expression
| multiplicative_expression '%' range_expression
;
모든 형태의 범위 연산자는 동일한 우선 순위를 갖습니다. 이 새 우선 순위 그룹은
..
연산자를 범위 연산자라고 합니다. 기본 제공 범위 연산자는 이 양식의 기본 제공 연산자 호출에 해당하는 것으로 대략적으로 이해할 수 있습니다.
System.Range operator ..(Index start = 0, Index end = ^0);
예제:
var array = new int[] { 1, 2, 3, 4, 5 };
var slice1 = array[2..^3]; // array[new Range(2, new Index(3, fromEnd: true))]
var slice2 = array[..^3]; // array[Range.EndAt(new Index(3, fromEnd: true))]
var slice3 = array[2..]; // array[Range.StartAt(2)]
var slice4 = array[..]; // array[Range.All]
또한 System.Index
은 System.Int32
로부터 암시적 변환이 있어야 하며, 이는 다차원 서명을 다루는 과정에서 정수와 인덱스를 혼합할 필요를 피하기 위함입니다.
기존 라이브러리 형식에 인덱스 및 범위 지원 추가
암시적 인덱스 지원
언어는 다음 조건을 충족하는 형식에 대해 Index
형식의 단일 매개 변수를 인스턴스 인덱서 멤버에 제공합니다.
- 셀 수 있는 형식입니다.
- 형식에는 하나의
int
인수를 받는 액세스 가능한 인스턴스 인덱서가 있습니다. - 형식에는
Index
첫 번째 매개 변수로 사용하는 액세스 가능한 인스턴스 인덱서가 없습니다.Index
유일한 매개 변수이거나 나머지 매개 변수는 선택 사항이어야 합니다.
형식은 액세스 가능한 getter와 반환 형식이 Index
형식의 표현식을 int
로 변환하기 위해 이 속성을 사용할 수 있으며, 이 과정에서 Index
형식을 전혀 사용할 필요가 없습니다.
Length
및 Count
모두 있는 경우 Length
선호됩니다. 향후 간단히 하기 위해, 제안에서는 Length
또는 Count
를 나타내기 위해 Length
이라는 이름을 사용할 것입니다.
이러한 형식의 경우, 언어는 T this[Index index]
형식의 인덱서 멤버가 존재하는 것처럼 작동합니다. 여기서 T
은 int
기반 인덱서의 반환 형식이며, ref
스타일 주석이 포함됩니다. 새 멤버는 get
인덱서와 접근성이 일치하는 동일한 set
및 int
멤버를 갖습니다.
새 인덱서는 Index
형식의 인수를 int
변환하고 int
기반 인덱서에 대한 호출을 내보내서 구현됩니다. 토론을 위해 receiver[expr]
예제를 사용하겠습니다.
expr
int
변환은 다음과 같이 발생합니다.
- 인수가
^expr2
의 형식이고expr2
의 유형이int
일 경우,receiver.Length - expr2
로 변환됩니다. - 그렇지 않으면
expr.GetOffset(receiver.Length)
로 변환됩니다.
특정 변환 전략에 관계없이 평가 순서는 다음과 같아야 합니다.
-
receiver
평가됩니다. -
expr
평가됩니다. - 필요한 경우
length
가 평가됩니다. -
int
기반 인덱서가 호출됩니다.
이렇게 하면 개발자가 수정할 필요 없이 기존 형식에서 Index
기능을 사용할 수 있습니다. 예를 들어:
List<char> list = ...;
var value = list[^1];
// Gets translated to
var value = list[list.Count - 1];
receiver
및 Length
식은 부작용이 한 번만 실행되도록 적절하게 분산됩니다. 예를 들어:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int this[int index] => _array[index];
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
int i = Get()[^1];
Console.WriteLine(i);
}
}
이 코드는 "길이 3 가져오기"를 인쇄합니다.
암시적 범위 지원
언어는 다음 조건을 충족하는 형식에 대해 Range
형식의 단일 매개 변수를 인스턴스 인덱서 멤버에 제공합니다.
- 셀 수 있는 형식입니다.
- 형식에는
Slice
형식의 두 매개 변수가 있는int
이라는 액세스 가능한 멤버가 있습니다. - 형식에는 단일
Range
첫 번째 매개 변수로 사용하는 인스턴스 인덱서가 없습니다.Range
유일한 매개 변수이거나 나머지 매개 변수는 선택 사항이어야 합니다.
이러한 형식에서는 T this[Range range]
형태의 인덱서 멤버가 있는 것처럼 언어가 동작하여 바인딩됩니다. 여기서 T
은 Slice
스타일 주석을 포함한 ref
메서드의 반환 타입입니다. 새로운 멤버도 Slice
과 일치하는 접근성을 가집니다.
Range
기반 인덱서가 receiver
식에 바인딩되면 Range
식을 Slice
메서드에 전달되는 두 값으로 변환하여 낮아집니다. 토론을 위해 receiver[expr]
예제를 사용하겠습니다.
Slice
의 첫 번째 인수는 다음과 같은 방식으로 범위로 유형화된 식을 변환하여 가져옵니다.
-
expr
이(가)expr1..expr2
형식이고,expr2
은(는) 생략할 수 있으며,expr1
이(가)int
유형일 경우,expr1
로 내보내집니다. -
expr
이^expr1..expr2
형식인 경우(expr2
를 생략할 수 있을 때),receiver.Length - expr1
로 내보내집니다. -
expr
이..expr2
형식인 경우(expr2
를 생략할 수 있을 때),0
로 내보내집니다. - 그렇지 않으면
expr.Start.GetOffset(receiver.Length)
로 내보내집니다.
이 값은 두 번째 Slice
인수의 계산에 다시 사용됩니다. 이렇게 하면 start
이라고 합니다.
Slice
의 두 번째 인수는 범위로 입력된 식을 다음과 같은 방식으로 변환하여 얻습니다.
-
expr
이(가)expr1..expr2
형식이고,expr1
은(는) 생략할 수 있으며,expr2
이(가)int
유형일 경우,expr2 - start
로 내보내집니다. -
expr
이expr1..^expr2
형식인 경우(expr1
를 생략할 수 있을 때),(receiver.Length - expr2) - start
로 내보내집니다. -
expr
이expr1..
형식인 경우(expr1
를 생략할 수 있을 때),receiver.Length - start
로 내보내집니다. - 그렇지 않으면
expr.End.GetOffset(receiver.Length) - start
로 내보내집니다.
특정 변환 전략에 관계없이 평가 순서는 다음과 같아야 합니다.
-
receiver
평가됩니다. -
expr
평가됩니다. - 필요한 경우
length
가 평가됩니다. -
Slice
메서드가 호출됩니다.
receiver
, expr
및 length
식이 적절하게 분산되어 부작용이 한 번만 실행되도록 합니다. 예를 들어:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int[] Slice(int start, int length) {
var slice = new int[length];
Array.Copy(_array, start, slice, 0, length);
return slice;
}
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
var array = Get()[0..2];
Console.WriteLine(array.Length);
}
}
이 코드는 "Get Length 2"를 출력합니다.
언어는 다음과 같은 알려진 형식을 특수하게 처리합니다.
-
string
: 메서드Substring
Slice
대신 사용됩니다. -
array
: 메서드System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray
Slice
대신 사용됩니다.
대안
새 연산자(^
및 ..
)는 구문 설탕입니다. 이 기능은 System.Index
및 System.Range
팩터리 메서드에 대한 명시적 호출을 통해 구현할 수 있지만, 결과적으로 훨씬 더 많은 상용구 코드가 생성되며, 경험이 직관적이지 않을 것입니다.
IL 표현
이러한 두 연산자는 후속 컴파일러 계층에서 변경되지 않고 일반 인덱서/메서드 호출로 낮아지게 됩니다.
런타임 동작
- 컴파일러는 배열 및 문자열과 같은 기본 제공 형식에 대해 인덱서를 최적화하고 인덱싱을 적절한 기존 메서드로 낮출 수 있습니다.
- 음수 값으로 생성된 경우
System.Index
throw됩니다. -
^0
는 예외를 던지지 않으며, 대신 주어진 컬렉션이나 열거형의 길이로 변환됩니다. -
Range.All
의미상0..^0
동일하며 이러한 인덱스로 분해될 수 있습니다.
고려 사항
ICollection을 기반으로 인덱싱 가능한 항목 감지
이 동작의 영감은 컬렉션 이니셜라이저였습니다. 유형 구조를 사용하여 기능을 선택했음을 전달합니다. 컬렉션 이니셜라이저의 경우 형식은 인터페이스 IEnumerable
(제네릭이 아닌)을 구현하여 기능을 옵트인할 수 있습니다.
이 제안은 처음에 형식이 인덱싱 가능하다고 간주되기 위해 ICollection
을 구현하도록 요구했습니다. 하지만 다음과 같은 여러 가지 특별한 사례가 필요했습니다.
-
ref struct
: 인터페이스를 구현할 수 없지만Span<T>
같은 형식은 인덱스/범위 지원에 적합합니다. -
string
:ICollection
구현하지 않으며 해당 인터페이스를 추가하면 비용이 많이 듭니다.
즉, 특수한 대/소문자 구분이 이미 필요한 키 형식을 지원합니다.
string
의 특수한 대/소문자 구분은 언어가 다른 영역(foreach
소문자 변환, 상수 등)에서도 이 작업을 수행하므로 덜 주목할 만합니다. ref struct
의 특수 케이스는 전체 형식 클래스를 특별하게 처리하기 때문에 더 중요한 문제입니다. 그들은 단순히 Count
이라는 이름의 속성과 반환 형식이 int
인 경우 Indexable로 레이블이 붙습니다.
디자인은 속성 Count
/ Length
가 있고 반환 형식이 int
인 모든 유형이 인덱싱 가능하다고 말하도록 조정되었습니다. 이렇게 하면 string
및 배열의 경우에도 모든 특수 케이스 처리를 제거합니다.
개수만 감지
속성 이름 Count
또는 Length
를 검색하는 것은 디자인을 약간 복잡하게 만들 수 있습니다. 하지만 표준화할 형식을 하나만 선택하는 것만으로는 충분하지 않습니다. 결국 많은 형식을 제외하게 됩니다.
-
Length
사용: System.Collections 및 하위 네임스페이스의 거의 모든 컬렉션을 제외합니다. 이러한 것들은ICollection
에서 파생되는 경향이 있기 때문에 길이보다Count
을 선호합니다. -
Count
사용:string
, 배열,Span<T>
및 대부분의ref struct
기반 형식 제외
인덱서블 형식의 초기 검색에 대한 추가적인 복잡성은 다른 측면에서의 단순화보다 더 큽니다.
이름으로 'Slice' 선택
Slice
이름은 .NET에서 조각 스타일 작업에 대한 사실상 표준 이름으로 선택되었습니다. netcoreapp2.1부터 모든 범위 스타일 형식은 조각화 작업에 Slice
이라는 이름을 사용합니다. netcoreapp2.1 이전에는 실제로 예제로 삼을 슬라이싱의 예가 없습니다.
List<T>
, ArraySegment<T>
, SortedList<T>
같은 형식은 조각화에 이상적이었지만 형식이 추가될 당시에는 그 개념이 존재하지 않았습니다.
따라서 Slice
유일한 예이기 때문에 이름으로 선택되었습니다.
인덱스 대상 형식 변환
인덱서 식에서 Index
변환을 보는 또 다른 방법은 대상 형식 변환입니다. 마치 return_type this[Index]
양식의 멤버가 있는 것처럼 바인딩하는 대신, 언어는 int
에 대한 대상 형식화 변환을 할당합니다.
이 개념은 Countable 형식의 모든 멤버 액세스로 일반화될 수 있습니다.
Index
형식의 식이 인스턴스 멤버 호출의 인수로 사용되고 수신기가 Countable인 경우, 그 식은 대상 형식 int
로 변환됩니다. 이 변환에 적용할 수 있는 멤버 호출에는 메서드, 인덱서, 속성, 확장 메서드 등이 포함됩니다. 생성자만 수신기가 없으므로 제외됩니다.
Index
형식을 가진 식에 대한 대상 형식 변환은 다음과 같이 구현할 것입니다. 토론을 위해 receiver[expr]
예제를 사용할 수 있습니다.
-
expr
이^expr2
의 형식이고expr2
의 유형이int
인 경우,receiver.Length - expr2
로 변환됩니다. - 그렇지 않으면
expr.GetOffset(receiver.Length)
로 변환됩니다.
receiver
및 Length
식은 부작용이 한 번만 실행되도록 적절하게 분산됩니다. 예를 들어:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int GetAt(int index) => _array[index];
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
int i = Get().GetAt(^1);
Console.WriteLine(i);
}
}
이 코드는 "길이 3 가져오기"를 인쇄합니다.
이 기능은 인덱스로 표시된 매개 변수가 있는 모든 멤버에게 유용합니다. 예를 들어 List<T>.InsertAt
. 언어가 어떤 표현이 인덱싱을 위한 것인지 여부에 대한 지침을 제공하지 못하므로, 이로 인해 혼동이 발생할 수 있습니다. Countable 유형에서 멤버를 호출할 때 할 수 있는 것은 Index
식을 int
으로 변환하는 것뿐입니다.
제한:
- 이 변환은
Index
형식의 식이 직접적으로 멤버의 인수가 되는 경우에만 적용됩니다. 중첩된 식에는 적용되지 않습니다.
구현 중에 내려진 결정
- 패턴의 모든 멤버는 인스턴스 멤버여야 합니다.
- Length 메서드를 찾았지만 반환 형식이 잘못된 경우, Count를 계속해서 찾습니다.
- 인덱스 패턴에 사용되는 인덱서에는 정확히 하나의 int 매개 변수가 있어야 합니다.
- 범위 패턴에 사용되는
Slice
메서드에는 정확히 두 개의 int 매개 변수가 있어야 합니다. - 패턴 멤버를 찾을 때 생성된 멤버가 아닌 원래 정의를 찾습니다.
디자인 회의
C# feature specifications