息抜き C# ~ New Normal なコードの書き方:第08回「リストと配列」 ~

こんにちはecbeingでアーキテクトをやっているC3POです。

New Normal なコードの書き方 の第08回目、今回は「リストと配列」の書き方についてご紹介しようと思います。

本記事は 息抜きC# 記事の第08回目です。
第07回「生文字列リテラル」 はこちら。


コレクション

リストと配列はC#の最初期から実装されていた機能で、みなさんもC#のコードを書いたことがあれば一度は利用したことがあると思います。 リストと配列はC#的には「コレクション」の一種ですので、この記事ではリストと配列をまとめて指す場合に「コレクション」とも表現することにします*1

近年このコレクションに目新しい機能が追加されましたので、それらの書き方について説明してまいります。

新しいインデックス

コレクション内部の要素を参照する際に指定するのが「インデックス」です。 古くは「添字」とも言いました*2

C#をやっていてインデックスがわからないという方はいないと思いますので、インデックスについてこれ以上の説明は不要でしょう。

新しいインデックスとして、C#8.0から「末尾インデックス」と「範囲インデックス」という2つのインデックスが追加されました。

末尾インデックス

末尾インデックスとは、通常のインデックスが先頭から数えるのとは逆に、末尾から数えるインデックスです。

【末尾インデックスのサンプル】

string[] words = {
            // 通常インデックス    末尾インデックス
    "マル", // 0                   ^4
    "ヒト", // 1                   ^3
    "フタ", // 2                   ^2
    "サン", // 3                   ^1
};          // 4 (or words.Length) ^0

Console.WriteLine(words[0]);    // 出力:マル
Console.WriteLine(words[3]);    // 出力:サン
Console.WriteLine(words[^1]);   // 出力:サン
Console.WriteLine(words[^4]);   // 出力:マル

// Console.WriteLine(words[^0]);   // 例外:System.IndexOutOfRangeException

末尾インデックスの特徴

  1. 末尾から逆順に指定するインデックス
  2. ^を付けて数字を指定する
  3. 最後の要素は^0ではなく^1
  4. ^0と書いてもコンパイルは通るが、IndexOutOfRange例外が発生する

末尾インデックスの登場により、今までwords[words.Length - 1]と書かなくてはならなかったのがwords[^1]で良くなりました。

書くのが簡単になりましたし、見た目にもスッキリしましたね。

範囲インデックス

範囲インデックスとは、コレクション内の連続した要素を、一度に取得するためのインデックスです。

【範囲インデックスのサンプル】

List<int> numbers = new() {0, 1, 2, 3, 4, 5 };

Console.WriteLine(string.Join(",", numbers));       // 出力:0,1,2,3,4,5
Console.WriteLine(string.Join(",", numbers[..2]));  // 出力:0,1
Console.WriteLine(string.Join(",", numbers[1..4])); // 出力:1,2,3
Console.WriteLine(string.Join(",", numbers[3..]));  // 出力:3,4,5

範囲インデックスの特徴

  • コレクションの中の範囲を指定することができるインデックス
  • 範囲の始まりを指定するのが「開始」オペランド、終わりを指定するのが「終了」オペランド
  • 「開始」で指定した要素は含まれるが、「終了」で指定した要素は含まれない
Console.WriteLine(string.Join(",", numbers));       // 出力:0,1,2,3,4,5
// 開始「1」の要素は含まれるが、終了「4」の要素は含まれない
Console.WriteLine(string.Join(",", numbers[1..4])); // 出力:1,2,3
  • 開始を省略するとコレクションの「最初の要素から」、終了を省略すると「最後の要素まで」、となる
// 元コレクション
Console.WriteLine(string.Join(",", numbers));       // 出力:0,1,2,3,4,5
// 開始が省略されているので、最初の要素から終了「3」の1つ前まで
Console.WriteLine(string.Join(",", numbers[..3]));  // 出力:0,1,2
// 終了が省略されているので、開始「3」から最後の要素まで
Console.WriteLine(string.Join(",", numbers[3..]));  // 出力:3,4,5
// 開始と終了が省略されているので、全要素となる
Console.WriteLine(string.Join(",", numbers[..]));   // 出力:0,1,2,3,4,5
  • 範囲インデックスを使うと、部分コレクションが生成される
List<int> numbers = new() { 0, 1, 2, 3, 4, 5 };
// 範囲インデックスは新しいコレクションを生成している
List<int> parts = numbers[1..4];
Console.WriteLine(string.Join(",", parts));     // 出力:1,2,3
  • 生成された部分コレクションはコピーなので元のコレクションに影響を与えない
// 部分コレクションを変更しても元のコレクションに影響は出ない
parts[1] = 100;
Console.WriteLine(string.Join(",", parts));     // 出力:1,100,3
Console.WriteLine(string.Join(",", numbers));   // 出力:0,1,2,3,4,5
  • コレクションのコピーのイディオム
List<int> duplicate = numbers[..];
  • 文字列の先頭と末尾を削除するイディオム
Console.WriteLine(",0,1,2,3,4,5,"[1..^1]);  // 出力:0,1,2,3,4,5


これまではコレクションの一部をコピーする場合、リストだとGetRange()を使用する必要があり、配列だとコピー先配列を用意してからArray.Copy()を使用する必要がありました *3

範囲インデックスだけでコレクションの部分コピーが作れるようになったことは、末尾インデックス以上に革新的です。

もしまだArray.Copy()で配列の部分コピーを作ってる、という方がいましたらぜひとも範囲インデックスの使用を検討してみて下さい。

コレクション生成の改良

コレクションの生成には今まで下記のような記述方法がありました。

【今までのコレクション生成のサンプル】

// 配列の生成
int[] array1;
array1 = new[] { 1, 2, 3 };
// リストの生成
List<int> b1 = new() { 1, 2, 3 };
// 配列の初期化(「new[]」が省略できる)
int[] array2 = { 1, 2, 3 };

配列の生成、リストの生成、配列の初期化、でそれぞれ書き方が異なっていました。

コレクション式

C#12.0で導入されたコレクション式は、配列やリストなどのコレクションをより簡潔に記述できます。

【コレクション式のサンプル】

// 配列の生成
int[] array1;
array1 = [1, 2, 3];
// リストの生成
List<int> b1 = [1, 2, 3];
// 配列の初期化
int[] array2 = [1, 2, 3];

コレクション式の特徴

  1. new[]new()を書かなくても良くなる
  2. 初期化でもそれ以外でも同じ書き方ができる

コレクション式のおかげでコレクション生成の際に、「newって書かなくていいんだっけ?書かなくちゃだめなんだっけ?」とか、「newの後ろって()だっけ?[]だっけ?」と迷うことが無くなります*4

おまけに見た目もシンプルで読みやすいので、コレクション式は非常にオススメの機能です。

スプレッド演算子

スプレッド演算子は範囲インデックスとよく似ていて*5..演算子を利用します*6。 この演算子はコレクション式の中で、他のコレクションを展開する機能を持ちます。

【スプレッド演算子のサンプル】

int[] a1 = [0, 1, 2, 3, 4];
int[] a2 = [5, 6, 7, 8, 9];
int[] a = [.. a1, .. a2];

Console.WriteLine(string.Join(",", a)); // 出力:0,1,2,3,4,5,6,7,8,9

スプレッド演算子の特徴

  1. コレクション式の中でしか使えない
  2. コレクション変数の前に..をつける
  3. 主にコレクションの連結に使われる

スプレッド演算子のおかげで、今までリストの「AddRange()」やLINQの「Concat()」を使って行っていたコレクションの連結が、とても簡単にかけるようになりました。 コレクション式を使うなら、スプレッド演算子も是非活用しましょう。

最後に

最後に「末尾インデックス」「範囲インデックス」「コレクション式」「スプレッド演算子」全てを使った例を見てみましょう*7

【全て使ったサンプル】

string s1 = "ルパン三世カリオストロの城";
string s2 = "ホーホケキョとなりの山田くん";
char[] s = [.. s2[6..10], s1[^4], .. s1[^4..^2]];

Console.WriteLine(s);   // 出力:となりのトトロ


ということで、最近のC#におけるリストと配列の新しい書き方の説明でした。

今まで配列の切り貼りなどは、やりたいことに比べてコードが煩雑になりがちでしたが、これら新しい書き方を駆使することで、簡単かつ読みやすくコーディングできるようになりました。

みなさんも新しい書き方を使い、書きやすく読みやすいコードを作ってみて下さい。

第09回目はこちら。(来年公開予定)

ecbeingでは書きやすく読みやすいコードに興味があるエンジニアを募集しています!!

careers.ecbeing.tech

*1:本当は「コレクション」にはディクショナリなども含まれるのですが、ここで説明し始めると長くなりすぎてしまうので、一旦「コレクション」はリストと配列だと考えて下さい

*2:最近「添字」という言葉を聞かないのは自分だけでしょうか?

*3:もうすこしスマートに書くとしてもLINQを使って「Skip(a).Take(b).ToArray()」のようにコーディングする必要がありました

*4:これ迷うの自分だけでしょうか・・・?

*5:スプレッド演算子と範囲インデックスはどちらもコレクション関連で「..」を使うので、非常に間違いやすいです。しかし、間違ったからと言って特に困ることもないので、そういうものだと思っておけば大丈夫です

*6:「..」はパターンマッチングのスライスパターンでも利用されていて、スプレッド演算子はこちらをかなり意識しているらしいです。が、パターンマッチングの話をし始めると長いので、この話はまたいつか

*7:文字列なら「Substring()」を使えば良いのでは?という非常に的確な疑問に関しては、あなたの胸の内に秘めておいて下さい・・・