次の方法で共有


範囲

メモ

この記事は機能仕様についてです。 仕様は、機能の設計ドキュメントとして使用できます。 これには、提案された仕様の変更および機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が決定され、現在の 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 型を使用する必要はまったくありません。 LengthCount の両方がある場合は、Length が優先して使用されます。 以降の説明を簡潔にするために、提案では Count または Length を表す名称として Length を使用しています。

このような型の場合、この言語は、T this[Index index] という形式のインデクサー メンバーがあるかのように動作します。ここで、T は、ref スタイルの注釈を含む int ベースのインデクサーの戻り値の型です。 新しいメンバーの getset には、int 型インデクサーのアクセス修飾子がそのまま引き継がれます。

新しいインデクサーは、Index 型の引数を int 型に変換し、int ベースのインデクサーを呼び出すことで実装されます。 説明のため、receiver[expr] の例で考えてみましょう。 expr から int 型への変換は次のようになります。

  • 引数が ^expr2 の形式で、expr2 の型が int の場合、この引数は receiver.Length - expr2 に変換されます。
  • それ以外の場合は、expr.GetOffset(receiver.Length) に変換されます。

特定の変換戦略に関係なく、評価は必ず以下の順序で行われます。

  1. receiver が評価されます。
  2. expr が評価されます。
  3. length は必要に応じて評価されます。
  4. 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 型の式を次のように変換して渡されます。

  • exprexpr1..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 型の式を次のように変換して渡されます。

  • exprexpr1..expr2 の形式 (expr1 は省略可能) で、expr2 の型が int の場合、expr2 - start として出力されます。
  • exprexpr1..^expr2 の形式 (expr1 は省略可能) の場合、(receiver.Length - expr2) - start として出力されます。
  • exprexpr1.. の形式 (expr1 は省略可能) の場合、receiver.Length - start として出力されます。
  • それ以外の場合は、expr.End.GetOffset(receiver.Length) - start として出力されます。

特定の変換戦略に関係なく、評価は必ず以下の順序で行われます。

  1. receiver が評価されます。
  2. expr が評価されます。
  3. length は必要に応じて評価されます。
  4. Slice メソッドが呼び出されます。

receiverexpr、および 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.IndexSystem.Range のファクトリ メソッドを明示的に呼び出すことでも実装できますが、その場合は多くの冗長なコードが生じて、直感的でなくなります。

IL 表現

これらの 2 つの演算子は、その後のコンパイラ層での変更なしに、通常のインデクサー/メソッド呼び出しに変換されます。

ランタイムの動作

  • コンパイラは配列や文字列などの組み込み型に対するインデクサーを最適化し、適切な既存のメソッドに変換できます。
  • System.Index では、負の値で構築された場合に例外がスローされます。
  • ^0 では例外がスローされませんが、提供されたコレクション/列挙可能オブジェクトの長さに変換されます。
  • Range.All0..^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 型パラメーターを受け取る必要がある
  • パターン メンバーを検索するときは、構築されたメンバーではなく元の定義を検索する

デザイン会議