次の方法で共有


コレクション式

この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。

機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。

機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。

チャンピオン号: https://github.com/dotnet/csharplang/issues/8652

概要

コレクション式では、共通のコレクション値を作成するための新しい簡潔な構文 ( [e1, e2, e3, etc]) が導入されています。 これらの値に他のコレクションをインライン化することは、..eのような[e1, ..c2, e2, ..c2]拡散要素を使用して可能です。

外部の BCL サポートを必要とせずに、いくつかのコレクションのような型を作成できます。 これらの型は次のとおりです。

型自体で直接採用できる新しい属性と API パターンによって、上記で説明されていないコレクションに似た型に対して、さらにサポートが提供されます。

モチベーション

  • コレクションに似た値は、プログラミング、アルゴリズム、特に C#/.NET エコシステムに非常に存在します。 ほぼすべてのプログラムは、これらの値を使用してデータを格納し、他のコンポーネントからデータを送受信します。 現在、ほぼすべての C# プログラムでは、このような値のインスタンスを作成するために、さまざまな、残念ながら詳細なアプローチを使用する必要があります。 一部の方法には、パフォーマンスの欠点もあります。 一般的な例を次に示します。

    • 配列。new Type[]値の前にnew[]または{ ... }が必要です。
    • スパン。 stackalloc やその他の面倒なコンストラクトを使用する場合があります。
    • コレクション初期化子。値の前に new List<T> (詳細な Tの推論がない) などの構文が必要であり、初期容量を指定せずに N .Add 呼び出しを使用するため、メモリの再割り当てが複数回発生する可能性があります。
    • 不変コレクション。値を初期化するために ImmutableArray.Create(...) などの構文が必要であり、中間割り当てとデータ コピーの原因となる可能性があります。 より効率的な建設形態( ImmutableArray.CreateBuilderなど)は扱いにくく、避けられないゴミを生み出します。
  • 周囲の生態系を見ると、リスト作成のあらゆる場所で、より便利で快適に使用できる例も見つかります。 TypeScript、Dart、Swift、Elm、Python など、広く使用され、大きな効果を得るために、この目的のために簡潔な構文を選びます。 カーソルの調査では、これらのリテラルを組み込んでこれらのエコシステムで生じる実質的な問題は明らかにされていません。

  • C# には、C# 11 の リスト パターン も追加されています。 このパターンでは、クリーンで直感的な構文を使用して、リストに似た値の照合と分解を行うことができます。 ただし、他のほぼすべてのパターンコンストラクトとは異なり、この一致/分解構文には対応する構築構文がありません。

  • 各コレクション型の構築に最適なパフォーマンスを得ることは難しい場合があります。 単純なソリューションでは、多くの場合、CPU とメモリの両方が無駄になります。 リテラル形式を使用すると、コンパイラの実装から最大限の柔軟性が得られるので、リテラルを最適化して、少なくともユーザーが提供できるのと同じくらい良い結果が得られますが、単純なコードを使用できます。 非常に多くの場合、コンパイラはより良い結果を得ることができ、仕様は、これを確実にするための実装戦略の観点から、実装に大量の余裕を持たせるようにすることを目的としています。

C# には包括的なソリューションが必要です。 コレクションに似た型と値の観点から、顧客の大部分のカッセを満たす必要があります。 また、言語も自然に感じられ、パターン マッチングで行われた作業を反映する必要があります。

これは、構文が[e1, e2, e3, e-etc][e1, ..c2, e2]と同等のパターンに対応する[p1, p2, p3, p-etc]または[p1, ..p2, p3]のようにする必要があるという自然な結論につながります。

詳細な設計

次の 文法 の運用環境が追加されます。

primary_no_array_creation_expression
  ...
+ | collection_expression
  ;

+ collection_expression
  : '[' ']'
  | '[' collection_element ( ',' collection_element )* ']'
  ;

+ collection_element
  : expression_element
  | spread_element
  ;

+ expression_element
  : expression
  ;

+ spread_element
  : '..' expression
  ;

コレクション リテラルは ターゲット型です

仕様の明確化

  • 簡潔にするために、次のセクションでは、 collection_expression を "リテラル" と呼びます。

  • expression_element インスタンスは、通常、 e1e_nなどと呼ばれます。

  • spread_element インスタンスは、通常、 ..s1..s_nなどと呼ばれます。

  • スパンの種類 は、 Span<T> または ReadOnlySpan<T>を意味します。

  • リテラルは通常、任意の数の要素を任意の順序で伝える [e1, ..s1, e2, ..s2, etc] として表示されます。 重要なことに、このフォームは次のようなすべてのケースを表すために使用されます。

    • 空のリテラル []
    • expression_elementのないリテラル。
    • spread_elementのないリテラル。
    • 任意の要素型の任意の順序を持つリテラル。
  • ..s_nは、s_nで反復処理される式として使用されたかのように決定されるforeach_statementの型です。

  • __nameで始まる変数は、nameの評価結果を表すために使用され、1 回だけ評価されるように場所に格納されます。 たとえば、 __e1e1の評価です。

  • List<T>IEnumerable<T>など) は、 System.Collections.Generic 名前空間内のそれぞれの型を参照します。

  • この仕様では、リテラルから既存の C# コンストラクトへの 変換 が定義されています。 クエリ式の翻訳と同様に、リテラル自体は、翻訳によって法的コードが生成される場合にのみ有効です。 この規則の目的は、暗黙的な言語の他の規則を繰り返す必要を回避することです (たとえば、格納場所に割り当てられた場合の式の変換可能性について)。

  • 以下で指定したとおりにリテラルを変換する実装は必要ありません。 同じ結果が生成され、結果の生成に観測可能な違いがない場合、翻訳は有効です。

    • たとえば、実装では、 [1, 2, 3] などのリテラルを直接、生データをアセンブリにベイク処理する new int[] { 1, 2, 3 } 式に直接変換し、各値を割り当てる __index または一連の命令が必要になります。 重要なのは、これは、翻訳のいずれかのステップが実行時に例外を引き起こす可能性がある場合に、プログラムの状態が翻訳によって示された状態のままであることを意味します。
  • "スタック割り当て" への参照は、ヒープではなくスタックに割り当てる戦略を指します。 重要なのは、その戦略が実際の stackalloc メカニズムを通じて行われるという意味や必要はないということです。 たとえば、 インライン配列 を使用することは、スタック割り当てを可能な場合に実現するための、許可された望ましいアプローチでもあります。 C# 12 では、インライン配列をコレクション式で初期化できないことに注意してください。 これはオープンな提案のままです。

  • コレクションは適切に動作すると見なされます。 例えば次が挙げられます。

    • コレクションの Count の値は、列挙時に要素の数と同じ値を生成することを前提としています。
    • System.Collections.Generic名前空間で定義されているこの仕様で使用される型は、副作用のないものと見なされます。 そのため、コンパイラは、このような型が中間値として使用される可能性があるが、それ以外の場合は公開されないシナリオを最適化できます。
    • コレクションで適用可能な .AddRange(x) メンバーを呼び出すと、 x を反復処理し、その列挙値をすべて .Add を使用してコレクションに個別に追加するのと同じ最終的な値が得られます。
    • 適切に動作しないコレクションを含むコレクション リテラルの動作は未定義です。

Conversions

コレクション式の変換を使用すると、コレクション式を型に変換できます。

コレクション式から次の型への暗黙的なコレクション式変換が存在します。

  • 1 次元 配列型T[]、その場合、 要素型T
  • スパンの種類:
    • System.Span<T>
    • System.ReadOnlySpan<T>
      その場合、 要素の型 は次のようになります。 T
  • 適切な create メソッドを持つ。その場合、要素の型は、拡張メソッドではなく、 インスタンス メソッドまたは列挙可能なインターフェイスから決定されるGetEnumeratorです。
  • 次の場所実装する構造体またはSystem.Collections.IEnumerable
    • この型には、引数なしで呼び出すことができる適用可能なコンストラクターがあり、コンストラクターはコレクション式の場所からアクセスできます。

    • コレクション式に要素がある場合、 にはインスタンスまたは拡張メソッド Add があります。

      • メソッドは、単一の値引数を使用して呼び出すことができます。
      • メソッドがジェネリックの場合、型引数はコレクションと引数から推論できます。
      • このメソッドは、コレクション式の場所からアクセスできます。

      その場合、要素型は型の反復型です

  • インターフェイスの種類:
    • System.Collections.Generic.IEnumerable<T>
    • System.Collections.Generic.IReadOnlyCollection<T>
    • System.Collections.Generic.IReadOnlyList<T>
    • System.Collections.Generic.ICollection<T>
    • System.Collections.Generic.IList<T>
      その場合、 要素の型 は次のようになります。 T

暗黙的な変換は、型に要素型がある場合に存在しますT要素の場合Eᵢコレクション式に含まれます。

  • Eᵢ が式要素の場合、EᵢからTへの暗黙的な変換があります。
  • Eᵢ拡散要素である場合..SᵢSᵢからTへの暗黙的な変換があります。

コレクション式から多次元配列型へのコレクション式変換はありません。

コレクション式からの暗黙的なコレクション式変換がある型は、そのコレクション式の有効な ターゲット型 です。

コレクション式から、次の追加の暗黙的な変換が存在します。

  • null 許容値型T?コレクション式から値型へのコレクション式の変換T。 この変換は、Tの後に、からTへのT?です。

  • 参照型T型とからTへのUを返すに関連付けられたTがあります。 この変換は、Uの後に、からUへのTです。

  • インターフェイス型Iからへの型IV返すに関連付けられたIがあります。 この変換は、コレクション式からVへの変換の後に、 から V へのIです。

メソッドを作成する

create メソッドは、[CollectionBuilder(...)]属性で示されます。 この属性は、コレクション のインスタンスを構築するために呼び出されるメソッドのビルダーの型とメソッド を指定します。

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(
        AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface,
        Inherited = false,
        AllowMultiple = false)]
    public sealed class CollectionBuilderAttribute : System.Attribute
    {
        public CollectionBuilderAttribute(Type builderType, string methodName);
        public Type BuilderType { get; }
        public string MethodName { get; }
    }
}

属性は、 classstructref struct、または interfaceに適用できます。 属性は継承されませんが、基本 class または abstract classに適用できます。

ビルダーの型は、非ジェネリック classまたはstructである必要があります。

まず、適用可能な作成メソッドのセットCMが決定されます。
これは、次の要件を満たすメソッドで構成されます。

  • メソッドには、 [CollectionBuilder(...)] 属性で指定された名前が必要です。
  • メソッドはビルダー で直接定義する必要があります。
  • メソッドは staticする必要があります。
  • このメソッドには、コレクション式を使用する場所からアクセスできる必要があります。
  • メソッドの アリティ は、コレクション型の アリティ と一致する必要があります。
  • メソッドには、 System.ReadOnlySpan<E>型のパラメーターが 1 つ必要です。値渡しされます。
  • メソッドの戻り値の型からコレクション型への ID 変換暗黙的な参照変換、またはボックス化変換があります

基本型またはインターフェイスで宣言されたメソッドは無視され、 CM セットの一部ではありません。

CM セットが空の場合、コレクション型には要素型がないため、create メソッドがありません。 次の手順は適用されていません。

CM セット内の 1 つのメソッドのうち、からコレクション型Eへの ID 変換がある場合は、コレクション型の create メソッドです。 それ以外の場合、 コレクション型 には create メソッドがありません。

[CollectionBuilder]属性が、予期されるシグネチャを持つ呼び出し可能なメソッドを参照していない場合、エラーが報告されます。

宣言C<S0, S1, …>にビルダー C<T0, T1, …>が関連付けられているコレクション式の場合B.M<U0, U1, …>()ターゲット型のジェネリック型引数は、最も外側の包含型から最も内側の型まで、ビルダー メソッドに順番に適用されます。

create メソッドの span パラメーターは、scopedまたは[UnscopedRef]明示的にマークできます。 パラメーターが暗黙的または明示的に scoped場合、コンパイラ ヒープではなくスタック上のスパンにストレージを割り当てることができます。

たとえば、に使用できる ImmutableArray<T>を次に示します。

[CollectionBuilder(typeof(ImmutableArray), "Create")]
public struct ImmutableArray<T> { ... }

public static class ImmutableArray
{
    public static ImmutableArray<T> Create<T>(ReadOnlySpan<T> items) { ... }
}

上記の create メソッド を使用すると、 ImmutableArray<int> ia = [1, 2, 3]; を次のように出力できます。

[InlineArray(3)] struct __InlineArray3<T> { private T _element0; }

Span<int> __tmp = new __InlineArray3<int>();
__tmp[0] = 1;
__tmp[1] = 2;
__tmp[2] = 3;
ImmutableArray<int> ia =
    ImmutableArray.Create((ReadOnlySpan<int>)__tmp);

Construction

コレクション式の要素は、左から右の順序で 評価されます 。 各要素は 1 回だけ評価され、要素へのそれ以上の参照は、この初期評価の結果を参照します。

コレクション式の後続の要素が評価される前または後に、拡散要素を反復処理できます。

構築中に使用されるメソッドからスローされたハンドルされない例外はキャッチされず、構築のそれ以上の手順を妨げます。

LengthCount、および GetEnumerator は副作用がないと見なされます。


ターゲット型が、を実装する構造体またはSystem.Collections.IEnumerableであり、ターゲット型に create メソッドがない場合、コレクション インスタンスの構築は次のようになります。

  • 要素は順番に評価されます。 一部またはすべての要素は、前の手順ではなく、以下の手順 評価できます。

  • コンパイラは拡散要素式でカウント可能なプロパティ (既知のインターフェイスまたは型から同等のプロパティ) を呼び出すことによって、コレクション式の既知の長さを決定できます。

  • 引数なしで適用できるコンストラクターが呼び出されます。

  • 各要素の順序:

    • 要素が 式要素の場合は、該当する Add インスタンスまたは拡張メソッドが、要素 を引数として使用して呼び出されます。 (クラシック コレクション初期化子の動作とは異なり、要素の評価と Add の呼び出しは必ずしもインターリーブされません)。
    • 要素が スプレッド要素 の場合は、次のいずれかが使用されます。
      • 適用できる GetEnumerator インスタンスまたは拡張メソッドが 拡散要素式 で呼び出され、列挙子の各項目に対して、該当する Add インスタンスまたは拡張メソッドが コレクション インスタンス で呼び出され、その項目が引数として指定されます。 列挙子が IDisposableを実装する場合、例外に関係なく、列挙後に Dispose が呼び出されます。
      • 適用できるAddRangeインスタンスまたは拡張メソッドは、引数として spread 要素を使用してコレクション インスタンスで呼び出されます。
      • 適用できるCopyToインスタンスまたは拡張メソッドは、コレクション インスタンスとインデックスを引数として使用してintで呼び出されます。
  • 上記の構築手順では、該当するEnsureCapacityインスタンスまたは拡張メソッドを、容量引数を使用してコレクション インスタンスで 1 回以上呼び出int


ターゲット型が 配列スパンcreate メソッドを持つ型、または インターフェイスである場合、コレクション インスタンスの構築は次のようになります。

  • 要素は順番に評価されます。 一部またはすべての要素は、前の手順ではなく、以下の手順 評価できます。

  • コンパイラは拡散要素式でカウント可能なプロパティ (既知のインターフェイスまたは型から同等のプロパティ) を呼び出すことによって、コレクション式の既知の長さを決定できます。

  • 初期化インスタンスは次のように作成されます。

    • ターゲット型が 配列 で、コレクション式の 長さが既知の場合、配列は予想される長さで割り当てられます。
    • ターゲット型が スパン または create メソッドを持つ型で、コレクションの 長さが既知の場合は、連続するストレージを参照する、予想される長さのスパンが作成されます。
    • それ以外の場合は、中間ストレージが割り当てられます。
  • 各要素の順序:

    • 要素が 式要素の場合、初期化インスタンス インデクサー が呼び出され、評価された式が現在のインデックスに追加されます。
    • 要素が スプレッド要素 の場合は、次のいずれかが使用されます。
      • 既知のインターフェイスまたは型のメンバーが呼び出され、拡散要素式から初期化インスタンスに項目がコピーされます。
      • 適用できる GetEnumerator インスタンスまたは拡張メソッドが 、拡散要素式 で呼び出され、列挙子の各項目に対して初期化インスタンス インデクサー が呼び出され、現在のインデックスに項目が追加されます。 列挙子が IDisposableを実装する場合、例外に関係なく、列挙後に Dispose が呼び出されます。
      • 初期化インスタンスとCopyToインデックスを引数として使用して、適用できるインスタンスまたは拡張メソッドがintで呼び出されます。
  • 中間ストレージがコレクションに割り当てられた場合、コレクション インスタンスは実際のコレクション長で割り当てられ、初期化インスタンスの値がコレクション インスタンスにコピーされます。スパンが必要な場合、コンパイラは中間ストレージの実際のコレクション長のスパンを使用 できます 。 それ以外の場合、初期化インスタンスはコレクション インスタンスです。

  • ターゲット型に create メソッドがある場合は、スパン インスタンスを使用して create メソッドが呼び出されます。


手記: コンパイラは、コレクションへの要素の追加を 遅らせる か、後続の要素を評価するまで拡散要素の反復処理を 遅らせる 可能性があります。 (後続の拡散要素に、コレクションを割り当てる前にコレクションの予想される長さを計算できる カウント可能な プロパティがある場合)。逆に、遅延の利点がない場合、コンパイラはコレクションに要素を 熱心に 追加し、拡散要素を 熱心に 反復処理することがあります。

次のコレクション式について考えてみましょう。

int[] x = [a, ..b, ..c, d];

拡散要素bcがカウント可能な場合、コンパイラは、aが評価されるまで、bおよびcからの項目の追加を遅らせ、結果の配列を予想される長さで割り当てることができる可能性があります。 その後、コンパイラは、cを評価する前に、dから項目を熱心に追加できました。

var __tmp1 = a;
var __tmp2 = b;
var __tmp3 = c;
var __result = new int[2 + __tmp2.Length + __tmp3.Length];
int __index = 0;
__result[__index++] = __tmp1;
foreach (var __i in __tmp2) __result[__index++] = __i;
foreach (var __i in __tmp3) __result[__index++] = __i;
__result[__index++] = d;
x = __result;

空のコレクション リテラル

  • 空のリテラル [] には型がありません。 ただし、 null リテラルと同様に、このリテラルは、任意の 構築可能な コレクション型に暗黙的に変換できます。

    たとえば、 ターゲットの種類 がなく、他の変換は関係しないため、次は適していません。

    var v = []; // illegal
    
  • 空のリテラルの拡散は省略できます。 例えば次が挙げられます。

    bool b = ...
    List<int> l = [x, y, .. b ? [1, 2, 3] : []];
    

    ここで、 b が false の場合、空のコレクション式に対して実際に値を構築する必要はありません。これは、最終的なリテラル内のゼロ値にすぐに分散されるためです。

  • 変更できないことがわかっている最終的なコレクション値を構築するために使用する場合、空のコレクション式はシングルトンであることが許可されます。 例えば次が挙げられます。

    // Can be a singleton, like Array.Empty<int>()
    int[] x = []; 
    
    // Can be a singleton. Allowed to use Array.Empty<int>(), Enumerable.Empty<int>(),
    // or any other implementation that can not be mutated.
    IEnumerable<int> y = [];
    
    // Must not be a singleton.  Value must be allowed to mutate, and should not mutate
    // other references elsewhere.
    List<int> z = [];
    

Ref safety

セーフ コンテキスト値の定義については、セーフ コンテキスト制約 (declaration-block、function-membercaller-context) を参照してください。

コレクション式 のセーフ コンテキスト は次のとおりです。

  • []空のコレクション式のセーフ コンテキストは、呼び出し元コンテキストです。

  • ターゲット型がスパン型System.ReadOnlySpan<T>Tがプリミティブ型の 1 つである場合boolsbytebyteshortushortcharintuintlongulongfloat、またはdouble、コレクション式に定数値のみが含まれる場合、コレクション式のセーフ コンテキストは呼び出し元コンテキストです。

  • ターゲットの型が スパン型System.Span<T> または System.ReadOnlySpan<T>の場合、コレクション式のセーフ コンテキストは 宣言ブロックです。

  • ターゲット型が create メソッドを持つ ref 構造体型の場合、コレクション式のセーフ コンテキストは、コレクション式がメソッドの span 引数である create メソッドの呼び出しのセーフ コンテキストです。

  • それ以外の場合、コレクション式のセーフ コンテキストは 呼び出し元コンテキストです。

宣言ブロックのセーフ コンテキストを持つコレクション式は、外側のスコープをエスケープできず、コンパイラはヒープではなくスタックにコレクションを格納できます

ref 構造体型のコレクション式が 宣言ブロックをエスケープできるようにするには、式を別の型にキャストすることが必要な場合があります。

static ReadOnlySpan<int> AsSpanConstants()
{
    return [1, 2, 3]; // ok: span refers to assembly data section
}

static ReadOnlySpan<T> AsSpan2<T>(T x, T y)
{
    return [x, y];    // error: span may refer to stack data
}

static ReadOnlySpan<T> AsSpan3<T>(T x, T y, T z)
{
    return (T[])[x, y, z]; // ok: span refers to T[] on heap
}

型の推定

var a = AsArray([1, 2, 3]);          // AsArray<int>(int[])
var b = AsListOfArray([[4, 5], []]); // AsListOfArray<int>(List<int[]>)

static T[] AsArray<T>(T[] arg) => arg;
static List<T[]> AsListOfArray<T>(List<T[]> arg) => arg;

型推論規則は、次のように更新されます。

最初のフェーズの既存のルールが新しい入力型推論セクションに抽出され、コレクション式式の入力型推論出力型推論にルールが追加されます。

11.6.3.2 第1フェーズ

各メソッド引数 Eᵢ:

  • 入力型の推論Eᵢするパラメーター型から行われますTᵢ

入力型の推論は、次のように型ETされます。

  • Eが要素を持つEᵢで、T要素型を持つ型であるかTₑまたはTnull 許容値型でありT0?T0が要素型を持つ場合はTₑEᵢに対して次のようになります。
    • Eᵢ式要素の場合、入力型の推論EᵢTₑ行われます。
    • Eᵢ反復型を持つ拡散要素である場合Sᵢ下限推論はSᵢTₑ行われます。
  • [最初のフェーズからの既存のルール] ...

11.6.3.7 出力型の推論

出力型の推論次のように型EtoTされます。

  • Eが要素を持つEᵢで、T要素型を持つ型であるかTₑまたはTnull 許容値型でありT0?T0が要素型を持つ場合はTₑEᵢに対して次のようになります。
    • Eᵢ式要素の場合、出力型の推論EᵢTₑ行われます。
    • Eᵢスプレッド要素の場合、Eᵢから推論は行われません。
  • [出力型推論からの既存のルール] ...

拡張メソッド

拡張メソッド呼び出し規則に変更はありません。

12.8.10.3 拡張メソッドの呼び出し

拡張メソッド Cᵢ.Mₑ は、次の場合 に使用できます

  • ...
  • expr から Mₑ の最初のパラメーターの型への暗黙的な ID、参照、またはボックス化の変換が存在します。

コレクション式には自然型がないため、 からの既存の変換は適用できません。 その結果、コレクション式を拡張メソッド呼び出しの最初のパラメーターとして直接使用することはできません。

static class Extensions
{
    public static ImmutableArray<T> AsImmutableArray<T>(this ImmutableArray<T> arg) => arg;
}

var x = [1].AsImmutableArray();           // error: collection expression has no target type
var y = [2].AsImmutableArray<int>();      // error: ...
var z = Extensions.AsImmutableArray([3]); // ok

オーバーロードの解決

コレクション式の変換 で特定のターゲット型を優先するように、式からのより適切な変換が更新されます。

更新されたルールでは、次の操作を行います。

  • span_typeは次のいずれかです。
    • System.Span<T>
    • System.ReadOnlySpan<T>
  • array_or_array_interfaceは次のいずれかです。
    • 配列型
    • 配列型によって実装される次のいずれかのインターフェイス
      • System.Collections.Generic.IEnumerable<T>
      • System.Collections.Generic.IReadOnlyCollection<T>
      • System.Collections.Generic.IReadOnlyList<T>
      • System.Collections.Generic.ICollection<T>
      • System.Collections.Generic.IList<T>

C₁式から型E に変換する暗黙的な変換 T₁ と、 C₂ 式から型 Eに変換する暗黙的な変換 T₂ を考えると、 次のいずれかが保持されている場合、 C₁ よりも になります。

  • Eコレクション式 であり、次のいずれかが保持されます。
    • T₁System.ReadOnlySpan<E₁>され、 T₂System.Span<E₂>され、 E₁ からへの暗黙的な変換が存在します。 E₂
    • T₁System.ReadOnlySpan<E₁>またはSystem.Span<E₁>であり、T₂要素型を持つarray_or_array_interfaceでありE₂E₁からE₂
    • T₁span_typeではなく、 T₂span_typeではなく、 T₁ から への暗黙的な変換が存在します。 T₂
  • Eコレクション式 ではなく、次のいずれかが保持されます。
  • E はメソッド グループです。...

配列初期化子とコレクション式の間のオーバーロード解決の違いの例:

static void Generic<T>(Span<T> value) { }
static void Generic<T>(T[] value) { }

static void SpanDerived(Span<string> value) { }
static void SpanDerived(object[] value) { }

static void ArrayDerived(Span<object> value) { }
static void ArrayDerived(string[] value) { }

// Array initializers
Generic(new[] { "" });      // string[]
SpanDerived(new[] { "" });  // ambiguous
ArrayDerived(new[] { "" }); // string[]

// Collection expressions
Generic([""]);              // Span<string>
SpanDerived([""]);          // Span<string>
ArrayDerived([""]);         // ambiguous

スパンの種類

スパン型 ReadOnlySpan<T>Span<T> はどちらも 構築可能なコレクション型です。 これらのサポートは、 params Span<T>の設計に従います。 具体的には、パラメーター配列がコンパイラによって設定された制限内にある場合、これらのスパンのいずれかを構築すると 、スタック に配列 T[] が作成されます。 それ以外の場合、配列はヒープに割り当てられます。

コンパイラがスタックに割り当てることを選択した場合、その特定の時点でリテラルを stackalloc に直接変換する必要はありません。 たとえば、次のようになります。

foreach (var x in y)
{
    Span<int> span = [a, b, c];
    // do things with span
}

コンパイラは、stackallocの意味が同じでありSpanが維持されている限り、を使用することを翻訳できます。 たとえば、上記を次のように変換できます。

Span<int> __buffer = stackalloc int[3];
foreach (var x in y)
{
    __buffer[0] = a
    __buffer[1] = b
    __buffer[2] = c;
    Span<int> span = __buffer;
    // do things with span
}

コンパイラは、スタックへの割り当てを選択するときに、インライン 配列 (使用可能な場合) を使用することもできます。 C# 12 では、インライン配列をコレクション式で初期化できないことに注意してください。 この機能はオープンな提案です。

コンパイラがヒープに割り当てることを決定した場合、 Span<T> の変換は単純です。

T[] __array = [...]; // using existing rules
Span<T> __result = __array;

コレクション リテラル変換

コレクション式の各拡散要素のコンパイル時の型がカウント可能な場合、コレクション式には既知の長さがあります。

インターフェイス変換

変更できないインターフェイス変換

変更メンバー ( IEnumerable<T>IReadOnlyCollection<T>IReadOnlyList<T>) を含まないターゲット型を指定すると、そのインターフェイスを実装する値を生成するには、準拠した実装が必要です。 型が合成される場合、合成された型は、対象となったインターフェイスの種類に関係なく、これらすべてのインターフェイスと ICollection<T> および IList<T>を実装することをお勧めします。 これにより、パフォーマンスの最適化を明るくするために値によって実装されたインターフェイスをイントロスペクトするものも含め、既存のライブラリとの最大限の互換性が保証されます。

さらに、この値は、非ジェネリック ICollection インターフェイスと IList インターフェイスを実装する必要があります。 これにより、コレクション式は、データ バインディングなどのシナリオで動的なイントロスペクションをサポートできます。

準拠している実装は、次の場合に自由に行うことができます。

  1. 必要なインターフェイスを実装する既存の型を使用します。
  2. 必要なインターフェイスを実装する型を合成します。

どちらの場合も、使用される型は、厳密に必要なインターフェイスよりも大きなインターフェイス セットを実装できます。

合成された型は、必要なインターフェイスを適切に実装する任意の戦略を自由に使用できます。 たとえば、合成された型は要素自体を直接インライン化して、追加の内部コレクション割り当てが不要になる場合があります。 合成された型では、値を直接計算することを選択して、ストレージを使用することもできませんでした。 たとえば、index + 1[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]を返します。

  1. この値は、true (実装されている場合) および非ジェネリック ICollection<T>.IsReadOnlyIList.IsReadOnlyに対してクエリを実行したときにIList.IsFixedSizeを返す必要があります。 これにより、変更可能なビューを実装しているにもかかわらず、コレクションが変更不可であることをコンシューマーが適切に認識できるようになります。
  2. 値は、変更メソッド ( IList<T>.Add など) の呼び出しでスローする必要があります。 これにより、安全が確保され、変更できないコレクションが誤って変更されるのを防ぐことができます。

変更可能なインターフェイス変換

変更するメンバー (つまり、 ICollection<T> または IList<T>) を含むターゲット型を指定します。

  1. 値は、 List<T>のインスタンスである必要があります。

既知の長さの変換

既知の 長さを 持つことにより、結果の効率的な構築が可能になり、データのコピーがなく、結果に不要な余裕領域がない可能性があります。

既知の長さを持たないと、結果が作成されません。 ただし、追加の CPU コストとメモリ コストによってデータが生成され、最終的な宛先に移動する可能性があります。

  • 既知の 長さの リテラル [e1, ..s1, etc]の場合、翻訳は最初に次の値で始まります。

    int __len = count_of_expression_elements +
                __s1.Count;
                ...
                __s_n.Count;
    
  • そのリテラルのターゲット型 T 指定します。

    • Tが何らかのT1[]である場合、リテラルは次のように変換されます。

      T1[] __result = new T1[__len];
      int __index = 0;
      
      __result[__index++] = __e1;
      foreach (T1 __t in __s1)
          __result[__index++] = __t;
      
      // further assignments of the remaining elements
      

      実装では、他の方法を使用して配列を設定できます。 たとえば、 .CopyTo()などの効率的な一括コピーメソッドを利用します。

    • Tが何らかのSpan<T1>の場合、リテラルは上記と同じように変換されます。ただし、__result初期化は次のように変換されます。

      Span<T1> __result = new T1[__len];
      
      // same assignments as the array translation
      

      stackalloc T1[] が維持されている場合、翻訳ではではなく、new T1[]またはインライン配列を使用できます。

    • Tが何らかのReadOnlySpan<T1>の場合、リテラルはSpan<T1>の場合と同じように変換されます。ただし、最終的な結果はSpan<T1>ReadOnlySpan<T1>点が異なります。

      ReadOnlySpan<T1>がプリミティブ型であり、すべてのコレクション要素が定数であるT1は、そのデータをヒープ上またはスタック上に配置する必要はありません。 たとえば、実装では、このスパンをプログラムのデータ セグメントの一部への参照として直接構築できます。

      上記の形式 (配列とスパンの場合) は、コレクション式の基本表現であり、次の変換規則に使用されます。

      • Tに対応する C<S0, S1, …> を持つがある場合B.M<U0, U1, …>()リテラルは次のように変換されます。

        // Collection literal is passed as is as the single B.M<...>(...) argument
        C<S0, S1, …> __result = B.M<S0, S1, …>([...])
        

        create メソッドにはインスタンス化されたReadOnlySpan<T>の引数型が必要です。そのため、コレクション式を create メソッドに渡すときにスパンの変換規則が適用されます。

      • T がコレクション初期化子をサポートしている場合は、次のようになります。

        • T型に 1 つのパラメーター int capacityを持つアクセス可能なコンストラクターが含まれている場合、リテラルは次のように変換されます。

          T __result = new T(capacity: __len);
          __result.Add(__e1);
          foreach (var __t in __s1)
              __result.Add(__t);
          
          // further additions of the remaining elements
          

          注: パラメーターの名前は、 capacityする必要があります。

          このフォームを使用すると、リテラルは、内部ストレージを効率的に割り当てることができるように、新しく構築された型に要素の数を通知できます。 これにより、要素が追加されるにつれて無駄な再割り当てが回避されます。

        • それ以外の場合、リテラルは次のように変換されます。

          T __result = new T();
          
          __result.Add(__e1);
          foreach (var __t in __s1)
              __result.Add(__t);
          
          // further additions of the remaining elements
          

          これにより、ストレージの内部再割り当てを防ぐための容量の最適化は行われませんが、ターゲットの種類を作成できます。

不明な長さの翻訳

  • 不明なTリテラルのターゲット型指定します。

    • T がコレクション初期化子をサポートしている場合、リテラルは次のように変換されます。

      T __result = new T();
      
      __result.Add(__e1);
      foreach (var __t in __s1)
          __result.Add(__t);
      
      // further additions of the remaining elements
      

      これにより、可能な限り最小限の最適化を行いながら、任意の iterable 型の拡散が可能になります。

    • Tが何らかのT1[]の場合、リテラルのセマンティクスは次と同じです。

      List<T1> __list = [...]; /* initialized using predefined rules */
      T1[] __result = __list.ToArray();
      

      ただし、上記は非効率的です。中間リストを作成し、そこから最終的な配列のコピーを作成します。 実装では、これを自由に最適化できます。たとえば、次のようなコードを生成します。

      T1[] __result = <private_details>.CreateArray<T1>(
          count_of_expression_elements);
      int __index = 0;
      
      <private_details>.Add(ref __result, __index++, __e1);
      foreach (var __t in __s1)
          <private_details>.Add(ref __result, __index++, __t);
      
      // further additions of the remaining elements
      
      <private_details>.Resize(ref __result, __index);
      

      これにより、最小限の無駄とコピーが可能になり、ライブラリ コレクションに追加のオーバーヘッドが発生することはありません。

      CreateArrayに渡されるカウントは、無駄なサイズ変更を防ぐための開始サイズ ヒントを提供するために使用されます。

    • Tスパン型の場合、実装は上記のT[]戦略、または同じセマンティクスを持つ他の戦略に従う可能性がありますが、パフォーマンスは向上します。 たとえば、配列をリスト要素のコピーとして割り当てる代わりに、 CollectionsMarshal.AsSpan(__list) を使用してスパン値を直接取得できます。

サポートされていないシナリオ

コレクション リテラルは多くのシナリオで使用できますが、置き換えできないものがいくつかあります。 これらには次のものが含まれます。

  • 多次元配列 (例: new int[5, 10] { ... })。 ディメンションを含める機能はなく、すべてのコレクション リテラルは線形構造またはマップ構造のみです。
  • コンストラクターに特別な値を渡すコレクション。 使用されているコンストラクターにアクセスするための機能はありません。
  • 入れ子になったコレクション初期化子 (例: new Widget { Children = { w1, w2, w3 } })。 このフォームは、 Children = [w1, w2, w3]とは非常に異なるセマンティクスを持っているため、維持する必要があります。 前者の呼び出しは.Addで繰り返し.Children、後者は.Childrenに新しいコレクションを割り当てます。 .Children割り当てることができない場合は、後者のフォームを既存のコレクションに追加することを検討できますが、それは非常に混乱を招く可能性があるようです。

構文のあいまいさ

  • collection_literal_expressionを使用するコードの複数の法的構文解釈がある場合、2 つの "真の" 構文のあいまいさがあります。

    • spread_elementは、range_expressionではあいまいです。 技術的には次のものが考えられます。

      Range[] ranges = [range1, ..e, range2];
      

      これを解決するには、次のいずれかを行います。

      • 範囲が必要な場合は、ユーザーに (..e) のかっこを適用するか、開始インデックス 0..e を含める必要があります。
      • スプレッドに別の構文 ( ... など) を選択します。 スライス パターンとの一貫性がないため、これは残念です。
  • 真のあいまいさはありませんが、構文によって解析の複雑さが大幅に増加する場合が 2 つあります。 エンジニアリング時間を指定しても問題ありませんが、コードを見るときのユーザーのコグニティブ オーバーヘッドは引き続き増加します。

    • ステートメントまたはローカル関数の collection_literal_expressionattributes の間のあいまいさ。 考慮すべき点:

      [X(), Y, Z()]
      

      次のいずれかになります。

      // A list literal inside some expression statement
      [X(), Y, Z()].ForEach(() => ...);
      
      // The attributes for a statement or local function
      [X(), Y, Z()] void LocalFunc() { }
      

      複雑な先読みがないと、リテラル全体を消費せずに見分けることができないでしょう。

      これに対処するオプションは次のとおりです。

      • これを許可し、解析作業を行って、このようなケースのうちどれであるかを判断します。
      • これを禁止し、 ([X(), Y, Z()]).ForEach(...)のようなかっこでリテラルをラップする必要があります。
      • collection_literal_expression内のconditional_expressionnull_conditional_operationsの間のあいまいさ。 考慮すべき点:
      M(x ? [a, b, c]
      

      次のいずれかになります。

      // A ternary conditional picking between two collections
      M(x ? [a, b, c] : [d, e, f]);
      
      // A null conditional safely indexing into 'x':
      M(x ? [a, b, c]);
      

      複雑な先読みがないと、リテラル全体を消費せずに見分けることができないでしょう。

      注: これは、ターゲットの型指定がを通じて適用されるため、conditional_expressionsがなくても問題になります。

      他のユーザーと同様に、あいまいさを解消するためにかっこを必要とする可能性があります。 言い換えると、null_conditional_operationのように書かなければ、x ? ([1, 2, 3]) :解釈を推測します。 しかし、それはかなり残念に思えます。 この種のコードは書き込みに不合理とは思われず、人々を旅する可能性があります。

デメリット

  • これにより、既に用意されている無数の方法に基づいて、コレクション式の もう 1 つの形式 が導入されます。 これは、言語の複雑さが増します。 しかし、これにより、1 つの リング 構文を統一してそれらすべてをルール化することも可能になります。つまり、既存のコードベースを簡略化し、どこでも均一な外観に移動できます。
  • [...] の代わりに {...} を使用すると、配列とコレクション初期化子に既に使用されている構文から移動します。 具体的には、[...] の代わりに {...} を使用します。ただし、これは、リスト パターンを行ったとき、言語チームによって既に解決されました。 {...}リスト パターンを操作しようとしましたが、解決できない問題が発生しました。 このため、C# の新機能である [...] に移行しました。これは、多くのプログラミング言語で自然に感じられ、あいまいさなしで新たに始めることができました。 対応するリテラル形式として [...] を使用することは、最新の決定と補完的であり、問題なく作業できるクリーンな場所を提供します。

これにより、疟疡が言語に導入されます。 たとえば、次の両方が有効であり、(幸いにも) まったく同じことを意味します。

int[] x = { 1, 2, 3 };
int[] x = [ 1, 2, 3 ];

ただし、新しいリテラル構文によってもたらされる幅と一貫性を考慮して、新しい形式に移行することをお勧めします。 IDE の提案と修正は、その点で役立つ可能性があります。

Alternatives

  • 他にどのようなデザインが検討されていますか? これを行わない場合の影響は何ですか?

解決された質問

  • stackallocが使用できなく、反復型がプリミティブ型の場合、コンパイラはスタック割り当てにを使用する必要がありますか?

    解決策: いいえ。 stackalloc バッファーを管理するには、コレクション式がループ内にあるときにバッファーが繰り返し割り当てられないように、インライン配列に対して追加の作業が必要です。 コンパイラと生成されたコードの複雑さが、古いプラットフォームでのスタック割り当ての利点を上回ります。

  • Length/Count プロパティの評価と比較して、リテラル要素はどのような順序で評価する必要がありますか? 最初にすべての要素を評価し、次にすべての長さを評価する必要がありますか? または、要素、その長さ、次の要素などを評価する必要がありますか?

    解決策: 最初にすべての要素を評価し、その後、他のすべてがその後に続きます。

  • 不明な 長さの リテラルは、配列、スパン、Construct(配列/スパン) コレクションなどの 既知の長さを必要とするコレクション型を作成できますか? これは効率的に行うのが難しくなりますが、プールされた配列やビルダーの巧妙な使用によって可能になる可能性があります。

    解決策: はい。 不明な長 さのリテラルから修正長のコレクションを作成できます。 コンパイラは、可能な限り効率的な方法でこれを実装することが許可されています。

    このトピックの最初の説明を記録するために、次のテキストが存在します。

    ユーザーは、次のようなコードを使用して、 常に不明長さのリテラルを既知の長さに することができます。

    ImmutableArray<int> x = [a, ..unknownLength.ToArray(), b];
    

    ただし、一時ストレージの割り当てを強制する必要があるため、これは残念です。 これがどのように出力されるかを制御すれば、より効率的になる可能性があります。

  • collection_expression IEnumerable<T>またはその他のコレクション インターフェイスにターゲット型を指定できますか?

    例えば次が挙げられます。

    void DoWork(IEnumerable<long> values) { ... }
    // Needs to produce `longs` not `ints` for this to work.
    DoWork([1, 2, 3]);
    

    解決策: はい。リテラルは、I<T>実装する任意のインターフェイス型List<T>ターゲット型にすることができます。 たとえば、「 IEnumerable<long> 」のように入力します。 これは、 List<long> にターゲットを入力し、その結果を指定したインターフェイス型に割り当てるのと同じです。 このトピックの最初の説明を記録するために、次のテキストが存在します。

    ここでの未解決の質問は、実際に作成する基になる型を決定する点です。 1 つのオプションは、 params IEnumerable<T>の提案を確認することです。 ここでは、 params T[]で発生するのと同様に、値を渡す配列を生成します。

  • コンパイラはArray.Empty<T>()[]を出力できますか? 可能な限り割り当てを回避するために、これを行う必要がありますか?

    Yes. コンパイラは、これが有効であり、最終的な結果が変更できない場合に Array.Empty<T>() を出力する必要があります。 たとえば、 T[]IEnumerable<T>IReadOnlyCollection<T> 、または IReadOnlyList<T>を対象とします。 ターゲットが変更可能な場合 (Array.Empty<T>またはICollection<T>) には、IList<T>を使用しないでください。

  • コレクション初期化子を拡張して、非常に一般的な AddRange メソッドを探す必要がありますか? 基になる構築型で使用して、拡散要素の追加をより効率的に実行できます。 また、 .CopyTo などを探すこともできます。 これらのメソッドは、翻訳されたコードで直接列挙するのと比較して、余分な割り当て/ディスパッチを引き起こす可能性があるため、ここで欠点がある可能性があります。

    Yes. 実装では、他のメソッドを使用してコレクション値を初期化することが許可されています。これらのメソッドには適切に定義されたセマンティクスがあり、コレクション型は "適切に動作" する必要があると仮定されています。 ただし、実際には、一方向の利点 (一括コピー) にも悪影響が生じる可能性があるため(構造体コレクションのボックス化など)、実装は注意する必要があります。

    欠点がない場合は、実装を利用する必要があります。 たとえば、 .AddRange(ReadOnlySpan<T>) メソッドを使用します。

未解決の質問

  • 反復型が (定義によっては) "あいまい" である場合に、要素を推論できるようにする必要がありますか? 例えば次が挙げられます。
Collection x = [1L, 2L];

// error CS1640: foreach statement cannot operate on variables of type 'Collection' because it implements multiple instantiations of 'IEnumerable<T>'; try casting to a specific interface instantiation
foreach (var x in new Collection) { }

static class Builder
{
    public Collection Create(ReadOnlySpan<long> items) => throw null;
}

[CollectionBuilder(...)]
class Collection : IEnumerable<int>, IEnumerable<string>
{
    IEnumerator<int> IEnumerable<int>.GetEnumerator() => throw null;
    IEnumerator<string> IEnumerable<string>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}
  • コレクションリテラルを作成してすぐにインデックスを作成することは有効ですか? 注: これには、コレクション リテラルに 自然な型があるかどうかに関する未解決の質問に対する回答が必要です。

  • 膨大なコレクションのスタック割り当てによってスタックが吹き飛ぶ可能性があります。 コンパイラには、このデータをヒープに配置するためのヒューリスティックが必要ですか? この柔軟性を実現するには、言語を指定する必要がありますか? params Span<T>の仕様に従う必要があります。

  • ターゲットタイプの spread_elementが必要ですか? 次に例を示します。

    Span<int> span = [a, ..b ? [c] : [d, e], f];
    

    注: これは一般的に、一部の要素セットの条件付き包含を許可するために次の形式で表示される場合があります。条件が false の場合は何も表示されません。

    Span<int> span = [a, ..b ? [c, d, e] : [], f];
    

    この完全なリテラルを評価するには、内の要素式を評価する必要があります。 これは、 b ? [c] : [d, e]を評価できることを意味します。 ただし、この式をコンテキストで評価する対象の型が存在せず、 自然型の種類が存在しない場合、ここでは [c] または [d, e] で何を行うかを判断できません。

    これを解決するために、リテラルの spread_element 式を評価するときに、リテラル自体のターゲット型と同等の暗黙的なターゲット型があったと言えます。 したがって、上記では、次のように書き換えられます。

    int __e1 = a;
    Span<int> __s1 = b ? [c] : [d, e];
    int __e2 = f;
    
    Span<int> __result = stackalloc int[2 + __s1.Length];
    int __index = 0;
    
    __result[__index++] = a;
    foreach (int __t in __s1)
      __result[index++] = __t;
    __result[__index++] = f;
    
    Span<int> span = __result;
    

create メソッドを使用する構築可能なコレクション型の指定は、変換が分類されるコンテキストに依存します

この場合の変換の存在は、コレクション型の反復型の概念によって異なります。 ReadOnlySpan<T>であるTを受け取る create メソッドがある場合、変換は存在します。 それ以外の場合は、行われません。

ただし、 イテレーション型 は、 foreach が実行されるコンテキストに依存します。 同じ コレクション型 の場合、スコープ内の拡張メソッドに基づいて異なる場合があり、未定義にすることもできます。

これは、型がそれ自体で foreach 可能に設計されていない場合に、 foreach の目的で問題なく感じます。 その場合、拡張メソッドは、コンテキストに関係なく、型の foreach-ed の方法を変更できません。

しかし、そのようなコンテキストに依存する変換には、少し奇妙に感じます。 実質的に、変換は "不安定" です。 構築可能に明示的に設計されたコレクション型では、非常に重要な詳細 (反復型) の定義を除外できます。 型はそれ自体に "変換不可" のままにします。

例を次に示します。

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

[CollectionBuilder(typeof(MyCollectionBuilder), nameof(MyCollectionBuilder.Create))]
class MyCollection
{
}
class MyCollectionBuilder
{
    public static MyCollection Create(ReadOnlySpan<long> items) => throw null;
    public static MyCollection Create(ReadOnlySpan<string> items) => throw null;
}

namespace Ns1
{
    static class Ext
    {
        public static IEnumerator<long> GetEnumerator(this MyCollection x) => throw null;
    }
    
    class Program
    {
        static void Main()
        {
            foreach (var l in new MyCollection())
            {
                long s = l;
            }
        
            MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
                               2];
        }
    }
}

namespace Ns2
{
    static class Ext
    {
        public static IEnumerator<string> GetEnumerator(this MyCollection x) => throw null;
    }
    
    class Program
    {
        static void Main()
        {
            foreach (var l in new MyCollection())
            {
                string s = l;
            }
        
            MyCollection x1 = ["a",
                               2]; // error CS0029: Cannot implicitly convert type 'int' to 'string'
        }
    }
}

namespace Ns3
{
    class Program
    {
        static void Main()
        {
            // error CS1579: foreach statement cannot operate on variables of type 'MyCollection' because 'MyCollection' does not contain a public instance or extension definition for 'GetEnumerator'
            foreach (var l in new MyCollection())
            {
            }
        
            MyCollection x1 = ["a", 2]; // error CS9188: 'MyCollection' has a CollectionBuilderAttribute but no element type.
        }
    }
}

現在の設計では、型が 反復処理型 自体を定義していない場合、コンパイラは CollectionBuilder 属性のアプリケーションを確実に検証できません。 反復型がわからない場合は、create メソッドのシグネチャが何であるかわかりません。 イテレーション型がコンテキストから取得された場合、型が常に同様のコンテキストで使用されるという保証はありません。

パラメーター コレクション 機能もこの影響を受ける。 宣言ポイントで params パラメーターの要素型を確実に予測できないのは奇妙な気がします。 現在の提案では、 create メソッド が少なくとも paramscollection 型と同じくらいアクセス可能であることを確認する必要もあります。 コレクション型反復型自体を定義しない限り、このチェックを信頼性の高い方法で実行することはできません。

コンパイラ用に https://github.com/dotnet/roslyn/issues/69676 も開いています。これは基本的に同じ問題を観察しますが、最適化の観点からそれについて話します。

建議

CollectionBuilder属性を使用して、その反復処理の型自体を定義する型が必要です。 つまり、型は IEnumarable/IEnumerable<T>を実装するか、適切なシグネチャを持つパブリック GetEnumerator メソッドを持つ必要があります (これにより、拡張メソッドは除外されます)。

また、現時点では、 create メソッド は"コレクション式が使用されている場所でアクセス可能である" 必要があります。 これは、アクセシビリティに基づくコンテキスト依存関係のもう 1 つのポイントです。 このメソッドの目的は、ユーザー定義の変換メソッドの目的とよく似ています。また、パブリックである必要があります。 そのため、 create メソッド をパブリックにすることを検討する必要があります。

Conclusion

変更による承認 LDM-2024-01-08

イテレーション型の概念は、変換全体で一貫して適用されません

  • 次の場所実装する構造体またはSystem.Collections.Generic.IEnumerable<T>へ:
    • 要素ごとにEiへのTがあります。

この場合、T構造体またはクラス型の反復型が必要であると仮定しているように見えます。 ただし、その前提は正しくありません。 これは非常に奇妙な行動につながる可能性があります。 例えば次が挙げられます。

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public void Add(string l) => throw null;
    
    public IEnumerator<string> GetEnumerator() => throw null; 
}

class Program
{
    static void Main()
    {
        foreach (var l in new MyCollection())
        {
            string s = l; // Iteration type is string
        }
        
        MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
                           2];
        MyCollection x2 = new MyCollection() { "b" };
    }
}
  • を実装し、実装しないSystem.Collections.IEnumerableまたはクラス型System.Collections.Generic.IEnumerable<T>

実装は 反復型objectであると想定しているように見えますが、仕様はこの事実を未指定のままにし、各 要素 を何かに変換する必要はありません。 ただし、一般に、 反復型object 型では必要ありません。 これは、次の例で確認できます。

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable
{
    public IEnumerator<string> GetEnumerator() => throw null; 
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

class Program
{
    static void Main()
    {
        foreach (var l in new MyCollection())
        {
            string s = l; // Iteration type is string
        }
    }
}

反復型の概念は、Params コレクション機能の基本です。 そして、この問題は、2 つの機能の間に奇妙な不一致につながります。 例えば:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public IEnumerator<string> GetEnumerator() => throw null; 

    public void Add(long l) => throw null; 
    public void Add(string l) => throw null; 
}

class Program
{
    static void Main()
    {
        Test("2"); // error CS0029: Cannot implicitly convert type 'string' to 'long'
        Test(["2"]); // error CS1503: Argument 1: cannot convert from 'collection expressions' to 'string'
        Test(3); // error CS1503: Argument 1: cannot convert from 'int' to 'string'
        Test([3]); // Ok

        MyCollection x1 = ["2"]; // error CS0029: Cannot implicitly convert type 'string' to 'long'
        MyCollection x2 = [3];
    }

    static void Test(params MyCollection a)
    {
    }
}
using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public IEnumerator<string> GetEnumerator() => throw null; 
    public void Add(object l) => throw null;
}

class Program
{
    static void Main()
    {
        Test("2", 3); // error CS1503: Argument 2: cannot convert from 'int' to 'string'
        Test(["2", 3]); // Ok
    }

    static void Test(params MyCollection a)
    {
    }
}

一方または他方を揃えるのが良いでしょう。

建議

反復型の観点からまたはを実装し、各System.Collections.Generic.IEnumerable<T>System.Collections.IEnumerableを必要とする構造体またはクラスの変換可能性を指定しますEi反復型への変換。

Conclusion

承認済み LDM-2024-01-08

コレクション式の変換では、構築に最小限の API セットを使用できる必要がありますか?

変換に従って構築可能なコレクション型は、実際には構築できない可能性があり、予期しないオーバーロードの解決動作につながる可能性があります。 例えば次が挙げられます。

class C1
{
    public static void M1(string x)
    {
    }
    public static void M1(char[] x)
    {
    }
    
    void Test()
    {
        M1(['a', 'b']); // error CS0121: The call is ambiguous between the following methods or properties: 'C1.M1(string)' and 'C1.M1(char[])'
    }
}

ただし、'C1。M1(string)' は、次の理由で使用できる候補ではありません。

error CS1729: 'string' does not contain a constructor that takes 0 arguments
error CS1061: 'string' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)

ユーザー定義型と、有効な候補に言及しないより強力なエラーを含む別の例を次に示します。

using System.Collections;
using System.Collections.Generic;

class C1 : IEnumerable<char>
{
    public static void M1(C1 x)
    {
    }
    public static void M1(char[] x)
    {
    }

    void Test()
    {
        M1(['a', 'b']); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
    }

    public static implicit operator char[](C1 x) => throw null;
    IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

この状況は、変換を委任するメソッド グループとよく似ているようです。 つまり、変換が存在するが、誤ったシナリオがありました。 変換が間違っている場合は存在しないことを確認することで、それを改善することにしました。

"Params Collections" 機能では、同様の問題が発生します。 構築できないコレクションに対して params 修飾子の使用を禁止することをお勧めします。 ただし、現在の提案では、チェックは 変換 セクションに基づいています。 例を次に示します。

using System.Collections;
using System.Collections.Generic;

class C1 : IEnumerable<char>
{
    public static void M1(params C1 x) // It is probably better to report an error about an invalid `params` modifier
    {
    }
    public static void M1(params ushort[] x)
    {
    }

    void Test()
    {
        M1('a', 'b'); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
        M2('a', 'b'); // Ok
    }

    public static void M2(params ushort[] x)
    {
    }

    IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

この問題は、前に少し説明したように見えます。 https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressionsを参照してください。 その時点で、現在指定されているルールが補間文字列ハンドラーの指定方法と一致するという引数が作成されました。 見積もりを次に示します。

特に、補間文字列ハンドラーはもともとこのように指定されていましたが、この問題を考慮して仕様を改訂しました。

類似点がいくつかありますが、考慮する価値のある重要な区別もあります。 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md#interpolated-string-handler-conversionからの引用を次に示します。

Tは、System.Runtime.CompilerServices.InterpolatedStringHandlerAttributeに起因する場合はapplicable_interpolated_string_handler_typeと言われます。 interpolated_string_expressionからする暗黙的なT、または完全に_interpolated_string_expression_sで構成され、+演算子のみを使用するadditive_expressionが存在します。

ターゲット型には、型が挿入文字列ハンドラーになるという作成者の意図を示す強力なインジケーターである特別な属性が必要です。 属性の存在が偶然でないと仮定するのは公正です。 これに対し、型が "列挙可能" であるという事実は、型を構築可能にする作成者の意図があることを意味する必要はありません。 ただし、コレクション型属性で示される [CollectionBuilder(...)]が存在することは、型が構築可能であることを示す作成者の意図を示す強力なインジケーターのように感じます。

建議

を実装し、System.Collections.IEnumerable変換セクションがない構造体またはクラス型の場合は、少なくとも次の API が必要です。

  • 引数なしで適用できるアクセス可能なコンストラクター。
  • Addの値を引数として呼び出すことができる、アクセス可能なインスタンスまたは拡張メソッド。

Params Collectons 機能の目的上、このような型は、これらの API がパブリックとして宣言され、インスタンス (拡張) メソッドである場合に有効なparams型です。

Conclusion

変更による承認 LDM-2024-01-10