今日はリスト パターンの回でちょこっと出て来た []
リテラルの話。
逆に、リスト パターン側でも {}
ではなく []
を使う決断に至った理由でもあります。
もう実装があるリスト パターンと違って、こちらはまだ案が出たてで、 もしかしたら C# 11 よりもさらに後になるかもしれないです。
[] リテラルの導入
元々、C# よりも後に世に出たり、大幅改修したことがあるプログラミング言語には結構「コレクション リテラル」系の文法があります。
で、多くの場合、[ 1, 2, 3 ]
みたいに角括弧を利用。
そして現在の C# には new[] { 1, 2, 3 }
みたいな書き方はあるにはあるものの、いろんなコレクション型があって、それぞれ書き方に統一感がない状態。
// 型を明示、かつ、配列の時に限り {} だけで OK。
int[] array1 = { 1, 2, 3 };
// 型推論を使いたければ new[] {}。
var array2 = new[] { 1, 2, 3 };
// Target-typed new + コレクション初期化子。 () は省略不可。
List<int> list1 = new() { 1, 2, 3 };
// 通常の new + コレクション初期化子。こっちの場合は () 省略 OK。
var list2 = new List<int> { 1, 2, 3 };
// Span にはまあ、new で配列を割り当ててもいいものの、
// パフォーマンス的には stackalloc を使った方が大体の場合有利。
Span<int> span = stackalloc int[] { 1, 2 };
// ReadOnlySpan も同様。
// あと、stackalloc の後ろは型推論で省略可能。
ReadOnlySpan<int> ros = stackalloc[] { 1, 2, 3 };
// new() もコレクション初期化子も使えないかわいそうな型あり。
var immutable = System.Collections.Immutable.ImmutableArray.Create(1, 2, 3);
C# でももう少し統一感あるコレクション リテラルがあった方がいいし、
だったら他の言語に倣って []
を使った新文法を導入でいいのではないかという話になります。
// ぜんぶ [] にしたい。
int[] array1 = [ 1, 2, 3 ];
List<int> list1 = [ 1, 2, 3 ];
Span<int> span = [ 1, 2, 3 ];
ReadOnlySpan<int> ros = [ 1, 2 ];
System.Collections.Immutable.ImmutableArray<int> immutable = [ 1, 2, 3 ];
そしてこっち(リテラル側)でも []
を使うのであれば、
パターンの方で {}
(プロパティ パターンと区別が付かない)とか []{}
(new[]{}
との対称性はいいかもしれないもののキモい)とか考えず、そっちも素直に []
を使えばいいということに。
[] リテラル中の .. (spread 演算)
パターンの方で「[1, ..[2, 3, 4], 5]
と [1, 2, 3, 4, 5]
が同じ意味になる」と書きましたが、コレクション リテラル中でも同じく「入れ子のコレクションを展開」みたいな仕様があります。
int[] array = [ 1, 2, 3 ];
List<int> list = [ 0, ..array, 4 ]; // 0, 1, 2, 3, 4
他の言語で unpacking とか splat (* 記号が一部の人にそう呼ばれていて、この機能に * を使ってる言語ではこう呼ぶ)とか spread (拡散)演算子とか呼ばれているやつです。
C# ではまあ、LINQ の Concat
, Append
, Prepend
とかを使って同様のものは書けていましたが、煩雑、かつ、パフォーマンスはいまいちでした。
int[] array1 = { 1, 2, 3 };
int[] array2 = { 4, 5, 6 };
// enumerator のインスタンスが余計に new されたりで遅い。
var linq = array1.Concat(array2).Prepend(0).Append(7);
// 列挙も結構遅い。
foreach (var x in linq)
{
Console.WriteLine(x);
}
// LINQ のよりも速い実装になる予定(後述)。
// かつ、Preapend よりはだいぶわかりやすい。
var spread = [ 0, .. array1, .. array2, 7 ];
おまけ: {} 案
一時期はパターンの方も is {}
にしたいみたいな話もあったんですが。
元々配列初期化子が {}
ですし、コレクション初期化子も {}
になる案もなくはなかったです。
ただ、{}
の用途としては他に Expression blocks という提案も出ていて、それとの弁別が無理そうということで没。
展開結果
展開結果、基本的には「前から順に詰める」です。 配列の場合だと割かしシンプルで、例えば以下のような感じ。
int[] array1 = { 1, 2, 3 };
int[] array2 = { 4, 5, 6 };
// var spread = [ 0, .. array1, .. array2, 7 ];
var len = 1 + array1.Length + array2.Length + 1;
var spread = new int[len];
var i = 0;
spread[i++] = 0;
for (int j = 0; j < array1.Length; j++, i++) spread[i] = array1[j];
for (int j = 0; j < array2.Length; j++, i++) spread[i] = array2[j];
spread[i] = 7;
Span<T>
の場合には new T[]
のところを stackalloc T[]
に変更。
ReadOnlySpan<T>
の場合はいったん Span<T>
と同じ処理でデータを書き込んでから、最後に ReadOnlySpan<T>
に変換。
それ以外の型については「所定のパターンを満たすコンストラクターと Init
メソッドを呼ぶ」と言うことになっています。
capacity
という名前の引数があるコンストラクターがある場合はそれを、なければ引数なしコンストラクターを呼ぶvoid Init(T1)
があって、T1
がT[]
ならnew[]
で、T1
がSpan<T>
,ReadOnlySpan<T>
ならstackalloc[]
で一時バッファーを作ってからInit
メソッドに渡す
例えば Init(int[])
だけ持っている型だと以下のような感じ。
// A a = [ 1, 2, 3 ];
int[] tempA = { 1, 2, 3 };
A a = new();
a.Init(tempA);
class A
{
public void Init(int[] items) { }
}
capacity
コンストラクターと Init(ReadOnlySpan<int>)
を持つ型だと以下のような感じ。
// A a = [ 1, 2, 3 ];
ReadOnlySpan<int> tempA = stackalloc[] { 1, 2, 3 };
A a = new(3);
a.Init(tempA);
class A
{
public A(int capacity) { }
public void Init(ReadOnlySpan<int> items) { }
}
immutable コレクション初期化
ちょっと別の機能追加も必要なのでさらに不透明なんですが、
この []
リテラルは前に話した ImmutableArray
の初期化問題の解決策としても期待されています。
とりあえず、ImmutableArray
についても前節と同じルールで初期化を掛けることを考えます。
using System.Collections.Immutable;
// ImmutableArray<int> a = [ 1, 2, 3 ];
ReadOnlySpan<int> tempA = stackalloc[] { 1, 2, 3 };
ImmutableArray<int> a = new();
a.Init(tempA); // こういうメソッドを足したいという話。今はない。
こういう Init
メソッドを足せればいいわけですが、
immutable を名乗る以上、new()
とは別に呼ばれるとまずいという話になります。
で、そこはinit-only プロパティと同じ方式で乗り切りたいとのこと。
任意のメソッドに対して、new()
中、もしくは、直後にしか呼ばない・呼ばれない保証をコンパイラーがするような仕様(メソッドに対する init
修飾)があればいいわけで、そういう仕様も模索中とのこと。
struct ImmutableArray<T>
{
readonly T[] _items;
// init 修飾を付けたメソッドは new() 内、もしくは、直後でしか呼べないように、
// コンパイラーが呼び出し箇所をチェックする。
public init void Init(ReadOnlySpan<T> items)
{
// 本来、コンストラクター内でしか書き換えてはいけないはずのフィールドを、
// init 修飾子が付いたメソッド内に限り書き換え可能にする。
_items = items.ToArray();
}
}