LoginSignup
4
記事投稿キャンペーン 「2024年!初アウトプットをしよう」

配列を初期化する (List, IList, ImmutableArray, etc. も初期化する) 《C#12》《コレクション式》

Posted at

配列もListも、同じように初期化する

《動作環境》
.net8.0 / C#12

結論

配列の初期化、リストの初期化は以下の方法で書けます。

配列の初期化、リストの初期化
  int[] intList = [2, 3, 5, 7, 11, 13, 17, 19];
  List<int> intList = [2, 3, 5, 7, 11, 13, 17, 19];

コレクション式を積極的に使いましょう。


コレクション式の構文

(.net7.0 / C#11)までの環境から来た人は見慣れないかもしれませんが、[ 値リスト ]の書式でコレクションを表現できます。この時、配列なのかListなのかImmutableArrayなのか、どんなコレクションなのか意識せずに書けるのがコレクション式の最大のメリットです。
どのコレクション型になるかは、左辺から型推論されます。

[コレクション式] コレクションと呼べそうな物なら統一された書式で書ける
int[] intList = [2, 3, 5, 7, 11, 13];
List<int> intList = [2, 3, 5, 7, 11, 13];
ImmutableArray<int> intList = [2, 3, 5, 7, 11, 13];
ReadOnlySpan<int> intList = [2, 3, 5, 7, 11, 13];
HashSet<int> intSet = [2, 3, 5, 7, 11, 13];
[コレクション式] インタフェースでも受けられる
IList<int> intList = [2, 3, 5, 7, 11, 13];
IEnumerable<int> intList = [2, 3, 5, 7, 11, 13];
[コレクション式] 最後にカンマがあっても無くても構わない
int[] intList;
intList = [2, 3, 5, 7, 11, 13];
intList = [2, 3, 5, 7, 11, 13,];
[コレクション式] 入れ子でもよい
List<MyStruct[]> myStructs = 
[
    [new(1, 1.0), new(2, 1.0)],
    [new(1, 1.4), new(2, 1.4)],
    [new(1, 1.7), new(2, 1.7)],
];

readonly record struct MyStruct(int X, double Y);
[コレクション式] メソッド呼び出しの際の実引数にも使える
DoList([1, 2, 3, 4, 5]);
DoArray([1, 2, 3, 4, 5]);

void DoList(List<int> ints) { };
void DoArray(int[] ints) { };

コレクション式以前の世界

ここで、コレクション式導入以前の構文をおさらいしておきます。

《歴史》コレクション初期化子

コレクション初期化子はC#3.0時点で導入されており、もはや皆さん当然のように使っている必須機能だと思います。

[List] コレクション初期化子(C#3.0)
List<int> intList = new List<int>() { 1, 2, 3, 4, 5 };
[List] このコードの糖衣構文になっている
List<int> intList = new List<int>();
intList.Add(1);
intList.Add(2);
intList.Add(3);
intList.Add(4);
intList.Add(5);

いくつか省略記法もあります。

[List] 括弧は省略できる (C#3.0)
List<int> intList = new List<int> { 1, 2, 3, 4, 5 };
[List] ターゲット型オブジェクト作成(C#9.0)
List<int> intList = new() { 1, 2, 3, 4, 5 };
[List] var使用
var intList = new List<int> { 1, 2, 3, 4, 5 };
// ↓は不可
// var intList = new() { 1, 2, 3, 4, 5 };

《歴史》配列初期化子

配列初期化子については、初期バージョンのC#より用意されています。

[配列] C#初期
int[] values = new int[] { 1, 2, 3, 4, 5 };
[配列] C#初期
int[] values = { 1, 2, 3, 4, 5 };
[配列] 暗黙的型指定(C#3.0)
int[] values = new[] { 1, 2, 3, 4, 5 };
[配列] var使用
var values = new int[] { 1, 2, 3, 4, 5 };
// 配列の中身から int[] 型と推論される
var values = new[] { 1, 2, 3, 4, 5 };
// ↓は不可
// var values = { 1, 2, 3, 4, 5 };

コレクション式の長所

コレクション初期化子、配列初期化子をおさらいしたところで、本題の「コレクション式」の長所を紹介していきます。

一言でいってしまうと、

コレクション式の長所はズバリ
「意識しないで良い箇所を、意識せずにコーディングできる」こと

なのですが、それらの例を以下にいくつか挙げていきます。

《長所》統一感

コレクション式は、コレクションの種類に依らない統一感があります。

配列とリストを組み合わせてみた例では、
コレクション式を使わない場合、以下のような部分が統一感を欠いています。

  • new[],new() で異なる
  • 配列のnew[]を省ける箇所が外側のみ
[Before] 2次元初期化
// 配列の配列
// 外側のnew[]は省略できるが、内側のnew[]は省略できない。
int[][] intMatrix1 =
{
   new[] { 2, 3, 5, 7, 11, 13 },
   new[] { 1, 3, 5, 7, 11, 13 },
   new[] { 1, 1, 2, 3, 5, 8 },
};
// Listの配列
List<int>[] intMatrix2 =
{
   new() { 2, 3, 5, 7, 11, 13 },
   new() { 1, 3, 5, 7, 11, 13 },
   new() { 1, 1, 2, 3, 5, 8 },
};
// 配列のList
List<int[]> intMatrix3 =
new() {
   new[] { 2, 3, 5, 7, 11, 13 },
   new[] { 1, 3, 5, 7, 11, 13 },
   new[] { 1, 1, 2, 3, 5, 8 },
};
// ListのList
List<List<int>> intMatrix4 =
new() {
   new() { 2, 3, 5, 7, 11, 13 },
   new() { 1, 3, 5, 7, 11, 13 },
   new() { 1, 1, 2, 3, 5, 8 },
};
[After] 2次元初期化
// 配列の配列
int[][] intMatrix1 =
[
    [2, 3, 5, 7, 11, 13],
    [1, 3, 5, 7, 11, 13],
    [1, 1, 2, 3, 5, 8],
];
// Listの配列
List<int>[] intMatrix2 =
[
    [2, 3, 5, 7, 11, 13],
    [1, 3, 5, 7, 11, 13],
    [1, 1, 2, 3, 5, 8],
];
// 配列のList
List<int[]> intMatrix3 =
[
    [2, 3, 5, 7, 11, 13],
    [1, 3, 5, 7, 11, 13],
    [1, 1, 2, 3, 5, 8],
];
// ListのList
List<List<int>> intMatrix4 =
[
    [2, 3, 5, 7, 11, 13],
    [1, 3, 5, 7, 11, 13],
    [1, 1, 2, 3, 5, 8],
];

《長所》書ける場所が限られない

初期化時に限らずどこにでも同じように書けるのがコレクション式の強みです。

コレクション初期化子や配列初期化子は、省略記法が以下の箇所で異なっています。

  • 「変数宣言と同時」か「それ以外の箇所」か
  • ジェネリクスが絡んだ時、「配列」か「それ以外」か

以下、「再代入」「実引数」の2つの例で示します。

[Before] 再代入
int[] intList = { 2, 3, 5, 7, 11 };
// 再代入では new[]は省けない。
intList = new[] { 3, 5, 7, 11, 13 };
[After] 再代入
int[] intList = [2, 3, 5, 7, 11];
intList = [3, 5, 7, 11, 13];
[Before] 実引数
DoList(new() { 1, 2, 3, 4, 5 });
DoArray(new[] { 1, 2, 3, 4, 5 });

void DoList(List<int> ints) { };
void DoArray(int[] ints) { };
[Before] 実引数+ジェネリクス
// new()は使用できない (要素から型推論できない)
DoList(new List<int>() { 1, 2, 3, 4, 5 });
// new[]は使用できる (要素から型推論できる)
DoArray(new[] { 1, 2, 3, 4, 5 });

void DoList<T>(List<T> ints) { };
void DoArray<T>(T[] ints) { };
[After] 実引数
DoList([1, 2, 3, 4, 5]);
DoArray([1, 2, 3, 4, 5]);

void DoList(List<int> ints) { };
void DoArray(int[] ints) { };
[After] 実引数+ジェネリクス
DoList([1, 2, 3, 4, 5]);
DoArray([1, 2, 3, 4, 5]);
    
void DoList<T>(List<T> ints) { };
void DoArray<T>(T[] ints) { };

《長所》Immutableにも使用できる

Immutable系コレクションも、コレクション式では書けるようになりました。
コレクション初期化子はAdd()の糖衣構文なので、罠のような構文になってしまっていました。

[Before] ImmutableArrayの初期化
ImmutableArray<int> intList = ImmutableArray.Create(2, 3, 5, 7, 11, 13);
[Before] これは書いちゃダメ
// Add()メソッドの糖衣構文であるせいで上手く動作しない。
// でも、これは書けてしまう。(実行時例外)
ImmutableArray<int> intList2 = new() { 2, 3, 5, 7, 11, 13};
[After] ImmutableArrayの初期化
// これは正しく機能する
ImmutableArray<int> intList = [2, 3, 5, 7, 11, 13];

使いこなせていなかったSystem.Collections.Immutable

《長所》インターフェイス型でも受けられる

左辺がインターフェイス型の箇所にコレクション式を書いた場合には、適切な型のインスタンスが生成されます。
凡そ以下の方法でインスタンスを生成する型が決定されるようです。

  • 代表する実際の型が存在する場合、その型
    • 例) IList<T>型⇒List<T>
  • 上記以外の場合、そのインターフェイスに最適な匿名の型を自動生成
    • 例) IReadOnlyCollection<T>型⇒<>z__ReadOnlyArray<T>匿名型

インターフェイス型はインスタンス化できないので、コレクション初期化子では、具体的なコレクション型を指定してインスタンスをnewする必要があります。

[Before] インターフェースでインスタンスを受ける
IList<int> intList = new List<int> { 2, 3, 5, 7, 11, 13 };
//↓これは当然書けない。
//IList<int> intList2 = new() { 2, 3, 5, 7, 11, 13 };
[After] インターフェースでインスタンスを受ける
IList<int> intList = [2, 3, 5, 7, 11, 13];

コレクション式でできないこと

いくつかコレクション式でできないことを挙げておきます。
C#13以降で何らかの解決予定のものもあるようです。

《非対応》自然な型

コレクション式はそれだけでは特定のコレクション型を表さないので、特定のコレクション型として扱いたいときはキャスト構文で明示的に指定する必要があります。

[要キャスト] varで受けたいとき
// C#12時点では、自然なコレクション型を持たない。
var values = (int[])[1, 2, 3, 4, 5];
// ↓こう書いたら何かの型を表していいかも。
// var values = [1, 2, 3, 4, 5];
[要キャスト] 式中に書くとき
// キャストで型を明示する必要あり。
static bool IsSmallPrime(int x) => ((ReadOnlySpan<int>)[2, 3, 5, 7]).Contains(x);
// ↓こういう風に書けたら需要ある?
//static bool IsSmallPrime(int x) => [2, 3, 5, 7].Contains(x);

《非対応》複数引数のAdd

C#12時点ではDictionary<>型の構築に対応していません。
これは残念ポイントの一つかもしれません。しばらくの間はDicitionary<>型はいままで通りの初期化を行いましょう。
(これはC#13以降で対応されそうな機能の筆頭だと思います。)

[コレクション式が使えない] Dictionaryの初期化
// C#12時点でのコレクション式では構築不可
Dictionary<int, double> dict = new()
{
    {1, 1},
    {2, 4},
    {3, 10}
};

《非対応》真の多次元配列

コレクション式は真の多次元配列には使用できません。
入れ子のコレクション式はジャグ配列(あるいは他のコレクションとの混合)と見なされます。

[コレクション式が使えない] 多次元配列の初期化
// コレクション式では構築不可
int[,] mat =
{
    {1, 2, 3, 4},
    {3, 4, 5, 6}
};

まとめ

過去の記法に比べ、コレクション式は「統一感があり」かつ「簡潔な」構文です。
またこの記事では扱っていません、コンパイル後のコードの効率も悪くなさそうです。(今回ここまで扱いきれませんでした)
コレクション式を積極的に使っていきましょう

些細なことでも構いませんので、当記事の誤りなど見つけた方はコメント下さい。
時代遅れになってしまった機能等あれば註釈を入れるようにしていますので、未来から来られた方も遠慮なくコメントを残してください。


~今回書かなかったこと~
  • コレクション式から生成されるコードを深堀りしていない
    • まじめにやろうとすると中身の型を変えたり、コンパイル時に確定/非確定で分けたり場合の数が多そう
  • スプレッド記法 ..array については別の記事で書きたい
    • これも生成されるコードが気になる

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4