範囲
メモ
この記事は機能仕様についてです。 仕様は、機能の設計ドキュメントとして使用できます。 これには、提案された仕様の変更および機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が決定され、現在の ECMA 仕様に組み込まれるまで公開されます。
機能の仕様と行われた実装では、いくつかの違いがあることがあります。 これらの違いは、関連する言語設計ミーティング (LDM) メモに取り上げられています。
機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。
チャンピオンの課題: https://github.com/dotnet/csharplang/issues/185
まとめ
この機能では、System.Index
および System.Range
オブジェクトの構築と、実行時のコレクションのインデックス指定やスライス操作を可能にする 2 つの新しい演算子を提供します。
概要
既知の型とメンバー
System.Index
および System.Range
の新しい構文形式を使用するには、使用する構文形式に応じて新しい既知の型とメンバーが必要になる場合があります。
「ハット」演算子 (^
) を使用するには、次のものが必要です。
namespace System
{
public readonly struct Index
{
public Index(int value, bool fromEnd);
}
}
配列要素へのアクセスで System.Index
型を引数として使用するには、次のメンバーが必要です。
int System.Index.GetOffset(int length);
System.Range
の ..
構文を使用するには、System.Range
型に加えて、次のメンバーの 1 つ以上が必要です。
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
コンストラクターが常に必要です。 ただし、他のメンバーがあり、..
引数の 1 つ以上が省略されている場合は、適切なメンバーで代用できます。
最後に、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 式を紹介します。 この機能では、新しい単項プレフィックス「ハット」演算子が導入されます。 この演算子の単一オペランドは 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
が導入されます。 これは 2 つの式を受け取る二項中置演算子です。 いずれのオペランドも省略可能で (以下の例を参照)、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 と Range に新たに対応
暗黙的なインデックスに対応
この言語では、次の条件を満たす型に対して、Index
型の単一パラメーターを受け取るインスタンス インデクサー メンバーが提供されます。
- 型は Countable です。
- この型には、引数として
int
型を 1 つだけ受け取るアクセス可能なインスタンス インデクサーがあります。 - この型には、最初のパラメーターとして
Index
を取るアクセス可能なインスタンス インデクサーがありません。Index
は唯一のパラメーターであるか、残りのパラメーターが省略可能である必要があります。
アクセス可能な getter と の戻り値の型を持つ、 または Length
という名前のプロパティがある場合、その型は int
です。 この言語では、このプロパティを使用して式の中で Index
型の式を int
型に変換できます。Index
型を使用する必要はまったくありません。 Length
と Count
の両方がある場合は、Length
が優先して使用されます。 以降の説明を簡潔にするために、提案では Count
または Length
を表す名称として Length
を使用しています。
このような型の場合、この言語は、T this[Index index]
という形式のインデクサー メンバーがあるかのように動作します。ここで、T
は、ref
スタイルの注釈を含む int
ベースのインデクサーの戻り値の型です。 新しいメンバーの 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
式は、副作用が 1 回だけ実行されるように、必要に応じてスピルされます。 次に例を示します。
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);
}
}
このコードは「Get Length 3」を出力します。
暗黙範囲サポート
この言語では、次の条件を満たす型に対して、Range
型の単一パラメーターを受け取るインスタンス インデクサー メンバーが提供されます。
- 型は Countable です。
- その型に、2 つの
int
型パラメーターを受け取る、Slice
という名前のアクセス可能なメンバーが定義されている。 - この型に、
Range
を最初のパラメーターとして受け取るインスタンス インデクサーがない。Range
は唯一のパラメーターであるか、残りのパラメーターが省略可能である必要があります。
このような型の場合、この言語では、T this[Range range]
という形式のインデクサー メンバーがあるかのようにバインドを行います。ここで、T
は、ref
スタイルの注釈を含む Slice
メソッドの戻り値の型です。 この新しいメンバーには、Slice
のアクセス修飾子が引き継がれます。
Range
ベースのインデクサーが receiver
という名前の式にバインドされる場合、Range
式は 2 つの値に変換され、Slice
メソッドに渡されます。 説明のため、receiver[expr]
の例で考えてみましょう。
Slice
の最初の引数は、range 型の式を次のように変換して渡されます。
expr
がexpr1..expr2
の形式 (expr2
は省略可能) で、expr1
の型がint
の場合、expr1
として出力されます。expr
が^expr1..expr2
の形式 (expr2
は省略可能) の場合、receiver.Length - expr1
として出力されます。expr
が..expr2
の形式 (expr2
は省略可能) の場合、0
として出力されます。- それ以外の場合は、
expr.Start.GetOffset(receiver.Length)
として出力されます。
この値は、2 番目の Slice
引数の計算で再利用されます。 その際、この値は start
として参照されます。 Slice
の 2 番目の引数は、range 型の式を次のように変換して渡されます。
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
式は、副作用が 1 回だけ実行されるように、必要に応じてスピルされます。 次に例を示します。
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 表現
これらの 2 つの演算子は、その後のコンパイラ層での変更なしに、通常のインデクサー/メソッド呼び出しに変換されます。
ランタイムの動作
- コンパイラは配列や文字列などの組み込み型に対するインデクサーを最適化し、適切な既存のメソッドに変換できます。
-
System.Index
では、負の値で構築された場合に例外がスローされます。 ^0
では例外がスローされませんが、提供されたコレクション/列挙可能オブジェクトの長さに変換されます。-
Range.All
は0..^0
と意味的に同等であり、これらのインデックスに分解できます。
考慮事項
ICollection に基づくインデックス操作可否の判定
この動作のヒントとなったのはコレクション初期化子でした。 型の構造を使用して、その型が特定の機能に対応していることを示すことができます。 コレクション初期化子の場合、ジェネリックではない IEnumerable
インターフェイスを実装することで、その型でのコレクション初期化の採用を伝えています。
この提案では、インデックス可能と認定されるためには、当初、型が ICollection
を実装する必要がありました。 ただし、これには対応が必要な特殊なケースが多数ありました。
-
ref struct
: これらはまだインターフェイスを実装できませんが、Span<T>
のような型はインデックス/範囲のサポートに最適です。 -
string
:ICollection
を実装せず、そのインターフェイスを追加するとコストが大きくなります。
つまり、キー タイプをサポートするには、特別な処理が必要になります。 string
の特殊な処理は、言語によって他の領域 (foreach
の組み込み、定数など) で実行されるため、それほど重要ではありません。ref struct
の特殊な処理は、型のクラス全体に対する特殊な処理であるため、より重要になります。 プロパティが Count
という名前で、戻り値の型が int
の場合、それらには「Indexable」とラベルが付けられます。
検討の結果、Count
/ Length
プロパティを持ち、戻り値の型が int
であるすべての型を Indexable とするように設計が正規化されました。 これにより、string
や配列の場合も含め、すべての特殊な処理が不要になります。
Count のみの検出
プロパティ名 Count
または Length
で検出すると、設計が少し複雑になります。 しかし、1 つの型のみを選択すると、多数の型が除外されてしまうため、標準化するには不十分です。
Length
を使用する: System.Collections とそのサブ名前空間のほぼすべてのコレクションが除外されます。 これらの型はICollection
から派生する傾向があるため、length よりもCount
が優先して使用されます。Count
を使用する:string
、配列、Span<T>
、およびほとんどのref struct
ベースの型が除外されます。
Indexable 型の初期検出で複雑さは増すものの、そのデメリットよりも他の側面での単純化というメリットが上回ります。
Slice という名前の選択
Slice
という名前は、.NET でスライス形式の操作の事実上の標準名称として定着していたため選択されました。 netcoreapp2.1 以降、すべての span style 型ではスライス操作に 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
式は、副作用が 1 回だけ実行されるように、必要に応じてスピルされます。 次に例を示します。
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);
}
}
このコードは「Get Length 3」を出力します。
この機能は、インデックスを表すパラメーターを受け取るすべてのメンバーに便利です。 たとえば、「 List<T>.InsertAt
」のように指定します。 また、この言語では、式がインデックス指定を意図しているかどうかについて何の指針も与えていないため、混乱が生じる可能性もあります。 この言語では、Countable 型のメンバーを呼び出す際に Index
型の式を int
型に変換することしかできません。
制限:
- この変換は、
Index
型の式がメンバーの直接的な引数となっている場合にのみ適用されます。 ネストされた式には適用されません。
実装時の決定事項
- パターン内のすべてのメンバーはインスタンス メンバーでなければならない
- Length メソッドが見つかったが戻り値の型が正しくない場合は、Count の検索を継続する
- Index パターンで使用されるインデクサーは、正確に 1 つの int 型パラメーターのみを受け取る必要がある
- Range パターンで使用される
Slice
メソッドは、正確に 2 つの int 型パラメーターを受け取る必要がある - パターン メンバーを検索するときは、構築されたメンバーではなく元の定義を検索する
デザイン会議
C# feature specifications