連載
» 2017年01月18日 05時00分 UPDATE

.NET TIPS:構文:コレクションのインスタンス化と同時に要素を追加するには?[C#/VB]

「コレクションを作成して、それにいちいち要素を追加して」というのは面倒だ。そうではなく、コレクションの作成と同時にその要素を追加する方法を解説する。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

対象:C# 3.0(Visual Studio 2008)/VB 10(Visual Studio 2010)以降


 コレクションをnewするとき、同時に要素も追加できる(正確には、「一文でコーディングできる」)。本稿では、その方法を解説するとともに、それとよく似た配列初期化子やインデックス初期化子についても紹介する。

 なお、掲載したサンプルコードの全てを試すにはVisual Studio 2015以降が必要である。

コレクションのインスタンス化と同時に要素を追加するには?

 コレクション初期化子を使えばよい(次のコード)。

List<string> list = new List<string>{"aaa", "bbb", "ccc", };

Dim list As List(Of String) = New List(Of String) From {"aaa", "bbb", "ccc"}

コレクション初期化子の例(上:C#、下:VB)
太字の部分がコレクション初期化子である。作成したコレクションのインスタンス(「list」変数)に、3つの要素を追加している。
C#では、コレクション初期化子を使う場合は「new List<string>」の後ろに「()」を付けなくてもよい。
VBでは、中かっこの前にFromキーワードを置く。また、追加する最後のオブジェクト(ここではNothing)の後ろに「,」を付けてはいけない(C#では最後に「,」を付けても構わない)。

文字列を追加する場合

 コレクション初期化子には、追加するオブジェクトを「,」(カンマ)で区切って並べる。

 分かりやすいのは上に示したような文字列を追加するケースだろう。実際にコンソールアプリを書いて試してみよう。コレクションの要素をコンソールに表示する「PrintCollection」メソッドを作り、それを使って想定通りの要素がコレクションに追加されたことを確認する(次のコード)。

 以降では、このコードをベースに解説していく。

using System.Collections;
using System.Collections.Generic;
using static System.Console;

class Program
{
  static void PrintCollection(IEnumerable col, string msg)
  {
    WriteLine(msg);
    int index = 0;
    foreach (var o in col)
      WriteLine($"[{index++}] {o?.ToString() ?? "(null)"}");
  }

  static void Main(string[] args)
  {
    List<string> list = new List<string> { "aaa", "bbb", null, };
    PrintCollection(list, "文字列のコレクション");
    // 出力:
    // 文字列のコレクション
    // [0] aaa
    // [1] bbb
    // [2] (null)

#if DEBUG
    ReadKey();
#endif
  }
}

Imports System.Console

Module Module1
  Sub PrintCollection(col As IEnumerable, msg As String)
    WriteLine(msg)
    Dim index As Integer = 0
    For Each o In col
      WriteLine($"[{index}] {If(o, "(Nothing)")}")
      index += 1
    Next
  End Sub

  Sub Main()
    Dim list As List(Of String) _
      = New List(Of String) From {"aaa", "bbb", Nothing}
    PrintCollection(list, "文字列のコレクション")
    ' 出力:
    ' 文字列のコレクション
    ' [0] aaa
    ' [1] bbb
    ' [2] (Nothing)

#If DEBUG Then
    ReadKey()
#End If
  End Sub
End Module

コレクション初期化子を使うコンソールアプリの例(上:C#、下:VB)
Mainメソッドの先頭で、文字列を格納するListコレクションを作り、そこに3つの要素を追加している。コレクション初期化子にnull/Nothingを与えた場合でも要素は追加される(追加された要素の内容がnull/Nothingになる)。
C#コードの冒頭にある「using static System.Console;」という書き方は、Visual Studio 2015からのものだ。詳しくは、「.NET TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]」をご覧いただきたい。同様な機能がVBには以前から備わっており、「.NET TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?」で解説している。
また、「Main」メソッド末尾にReadKeyメソッドを置く意味は、「.NET TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?」をご覧いただきたい。
「PrintCollection」メソッドに出てくる先頭に「$」記号が付いた文字列については、「.NET TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]」の後半(「C# 6.0/VB 14で追加された補間文字列機能を使用する」)を見てほしい。また、その中に埋め込んだC#コードの「o?.ToString() ?? "(null)"」という書き方は、「.NET TIPS:C#でnullチェックを簡潔に行うには?」と「.NET TIPS:構文:nullチェックを簡潔に記述するには?[C# 6.0]」で説明している。VBコードの同じ箇所に登場する「If(o, "(Nothing)")」という書き方は、If文ではなく、If演算子である(C#の三項演算子とNull条件演算子に相当)。

配列初期化子

 配列に関しては、コレクション初期化子と似た機能として、それ以前から配列初期化子がある(次のコード)。C#では特に意識する必要はないだろうが、VBではコレクション初期化子と書式が違うので注意が必要だ。

int[] array = new int[] { 1, 2, int.MaxValue, };
// 「new int[]」でサイズ指定(「new int[3]」)は不要(指定しても構わない)
PrintCollection(array, "整数の配列");
// 出力:
// 整数の配列
// [0] 1
// [1] 2
// [2] 2147483647

Dim array() As Integer = New Integer() {1, 2, Integer.MaxValue}
' 「New Integer()」で上限指定(「New Integer(2)」)は不要(指定しても構わない)
PrintCollection(array, "整数の配列")
' 出力:
' 整数の配列
' [0] 1
' [1] 2
' [2] 2147483647

配列初期化子の例(上:C#、下:VB)
VBでは、配列初期化子の場合はFromキーワードを書かない(書くとコンパイルエラーになる)。

 配列の宣言と初期化について詳しくは、「.NET TIPS:C#で配列を宣言するには?」「.NET TIPS:VB.NETで配列を宣言するには?」をご覧いただきたい。

一般的なクラスを格納するコレクションの場合

 冒頭のコレクション初期化子の例では、コレクションに文字列を追加した。実際には、何らかのクラスをインスタンス化し、さらに幾つかのプロパティを設定してからコレクションに追加することが多いだろう。

 例えば、次のコードに示す「SampleClass」クラスのインスタンスを作り、「Year」プロパティなどをセットしてからコレクションに追加したいとする。

class SampleClass
{
  public int Year { get; set; } = 2001;
  public int Month { get; set; } = 1;
  public int Day { get; set; } = 1;

  public override string ToString() => $"{Year:0000}/{Month:00}/{Day:00}";
}

Class SampleClass
  Public Property Year As Integer = 2001
  Public Property Month As Integer = 1
  Public Property Day As Integer = 1

  Public Overrides Function ToString() As String
    Return $"{Year:0000}/{Month:00}/{Day:00}"
  End Function
End Class

コレクションに格納する要素のクラス定義の例(上:C#、下:VB)
自動実装プロパティの宣言と同時に初期値を設定している(C#VB)。
また、C#コードのToStringメソッドは、ラムダ式で実装している。詳しくは「.NET TIPS:構文:メソッドやプロパティをラムダ式で簡潔に実装するには?[C# 6.0]」をご覧いただきたい。

 そのようなときは、オブジェクト初期化子を使って、追加するオブジェクトの生成と初期化もいっぺんに書いてしまえる(次のコード)。

List<SampleClass> list = new List<SampleClass>
  {
    new SampleClass { Year=2017, Month=1, Day=18 },
    new SampleClass { Year=2017, Month=2, Day=14 },
    new SampleClass { Year=2017, Month=3, },
    null,
  };
PrintCollection(list, "一般的なクラスのコレクション");
// 出力:
// 一般的なクラスのコレクション
// [0] 2017/01/18
// [1] 2017/02/14
// [2] 2017/03/01
// [3] (null)

Dim list As List(Of SampleClass) = New List(Of SampleClass) _
  From {
    New SampleClass With {.Year = 2017, .Month = 1, .Day = 18},
    New SampleClass With {.Year = 2017, .Month = 2, .Day = 14},
    New SampleClass With {.Year = 2017, .Month = 3},
    Nothing
  }
PrintCollection(list, "一般的なクラスのコレクション")
' 出力:
' 一般的なクラスのコレクション
' [0] 2017/01/18
' [1] 2017/02/14
' [2] 2017/03/01
' [3] (Nothing)

コレクション初期化子の中でオブジェクト初期化子を使う例(上:C#、下:VB)
太字の部分がオブジェクト初期化子である。VBではWithキーワードが必要だ。

 このオブジェクト初期化子については、「.NET TIPS:構文:インスタンス化と同時にプロパティを設定するには?[C#/VB]」で解説している。

Dictionaryコレクションの場合

 キーとバリューを格納するDictionaryクラスのように、要素を追加するときに複数の引数を要求するコレクションもある。そのときは、コレクション初期化子の中で引数のセットを中かっこで囲む(次のコード)。

Dictionary<int, SampleClass> dic = new Dictionary<int, SampleClass>
  {
    {1, new SampleClass { Year=2017, Month=1, Day=18 }},
    {2, new SampleClass { Year=2017, Month=2, Day=14 }},
    {3, new SampleClass { Year=2017, Month=3, }},
    {4, null },
  };
PrintCollection(dic, "Addメソッドに複数の引数が要求されるもの");
// 出力:
// Addメソッドに複数の引数が要求されるもの
// [0] [1, 2017/01/18]
// [1] [2, 2017/02/14]
// [2] [3, 2017/03/01]
// [3] [4, ]

Dim dic As Dictionary(Of Integer, SampleClass) _
  = New Dictionary(Of Integer, SampleClass) _
    From {
      {1, New SampleClass With {.Year = 2017, .Month = 1, .Day = 18}},
      {2, New SampleClass With {.Year = 2017, .Month = 2, .Day = 14}},
      {3, New SampleClass With {.Year = 2017, .Month = 3}},
      {4, Nothing}
    }
PrintCollection(dic, "Addメソッドに複数の引数が要求されるもの")
' 出力:
' Addメソッドに複数の引数が要求されるもの
' [0] [1, 2017/01/18]
' [1] [2, 2017/02/14]
' [2] [3, 2017/03/01]
' [3] [4, ]

コレクション初期化子を使ってDictionaryコレクションを初期化する例(上:C#、下:VB)
DictionaryクラスのAddメソッドは、キーとバリューの2つの引数を要求する。ここでは、キーとして1〜4の整数を、バリューとしてSampleClassオブジェクトを与えている。

 Dictionaryコレクションを初期化する方法について、詳しくは「.NET TIPS:Dictionaryクラスを簡単に初期化するには?[C# 3.0]」を参照してほしい。

インデックス初期化子(C#)

 Dictionaryコレクションを初期化するとき、C# 6.0ではインデックス初期化子を使っても上と同じ結果が得られる(次のコード)。

Dictionary<int, SampleClass> dic = new Dictionary<int, SampleClass>
{
  [5] = new SampleClass { Year = 2017, Month = 1, Day = 18 },
  [6] = new SampleClass { Year = 2017, Month = 2, Day = 14 },
  [7] = new SampleClass { Year = 2017, Month = 3, },
  [8] = null,
};
PrintCollection(dic, "インデックス初期化子(C# 6.0)");
// 出力:
// インデックス初期化子(C# 6.0)
// [0] [5, 2017/01/18]
// [1] [6, 2017/02/14]
// [2] [7, 2017/03/01]
// [3] [8, ]

インデックス初期化子を使ってDictionaryコレクションを初期化する例(C#)
書く手間はコレクション初期化子と大して変わらないが、インデクサーに対して値を設定していることが分かりやすい。コレクション初期化子とは混在できない。
なお、インデックス初期化子は、インデクサーを持っているクラスに対して機能する。そのクラスがIEnumerableインタフェースの実装やAddメソッドを持っている必要はない。

コレクション初期化子の動作を確かめる

 コレクション初期化子は、コレクションのインスタンスを作ってから、そのAddメソッドを呼び出して要素を追加する。

 その動作を確認するため、次のコードのようなコレクションクラスを作る。Listクラスに対して、コンストラクタが呼び出されたときとAddメソッドが呼び出されたときにコンソールにそのことを表示する機能を持たせたものだ。

class MyList<T> : List<T>
{
  public MyList() : base()
  {
    WriteLine($"コンストラクタ完了(要素数={this.Count})");
  }

  new public void Add(T item)
  {
    base.Add(item);
    WriteLine($"Addメソッド呼び出し完了: {item?.ToString() ?? "(null)"}
(要素数={this.Count}"); // 前の行に続けて1行に書く
  }
}

Class MyList(Of T)
  Inherits List(Of T)

  Public Sub New()
    MyBase.New()
    WriteLine($"コンストラクタ完了(要素数={Me.Count})")
  End Sub

  Public Overloads Sub Add(item As T)
    MyBase.Add(item)
    WriteLine($"Addメソッド呼び出し完了: {If(item IsNot Nothing, item.ToString(),
"(Nothing)")}(要素数={Me.Count})") ' 前の行に続けて1行に書く
  End Sub
End Class

コンストラクタとAddメソッドの呼び出しをレポートするコレクション(上:C#、下:VB)

 このMyListクラスを使うと、コレクション初期化子の動作が分かる(次のコード)。

WriteLine("実際にはインスタンス化した後でAddメソッドが呼び出される");
MyList<SampleClass> list = new MyList<SampleClass>
  {
    new SampleClass { Year=2017, Month=1, Day=18 },
    new SampleClass { Year=2017, Month=2, Day=14 },
    new SampleClass { Year=2017, Month=3, },
    null,
  };
// 出力:
// 実際にはインスタンス化した後でAddメソッドが呼び出される
// コンストラクタ完了(要素数=0)
// Addメソッド呼び出し完了: 2017/01/18(要素数=1)
// Addメソッド呼び出し完了: 2017/02/14(要素数=2)
// Addメソッド呼び出し完了: 2017/03/01(要素数=3)
// Addメソッド呼び出し完了: (null)(要素数=4)

WriteLine("実際にはインスタンス化した後でAddメソッドが呼び出される")
Dim list As MyList(Of SampleClass) = New MyList(Of SampleClass) _
  From {
    New SampleClass With {.Year = 2017, .Month = 1, .Day = 18},
    New SampleClass With {.Year = 2017, .Month = 2, .Day = 14},
    New SampleClass With {.Year = 2017, .Month = 3},
    Nothing
  }
' 出力:
' 実際にはインスタンス化した後でAddメソッドが呼び出される
' コンストラクタ完了(要素数=0)
' Addメソッド呼び出し完了: 2017/01/18(要素数=1)
' Addメソッド呼び出し完了: 2017/02/14(要素数=2)
' Addメソッド呼び出し完了: 2017/03/01(要素数=3)
' Addメソッド呼び出し完了: (Nothing)(要素数=4)

コレクション初期化子の動作を確認する(上:C#、下:VB)
コンストラクタの完了時点では「要素数=0」と報告された。その時点では、要素はまだ何も追加されていない。
その後、追加する要素の数だけ繰り返しAddメソッドが呼び出されている。

コレクション初期化子に対応した自作コレクション

 上で見たように、コレクション初期化子はAddメソッドの呼び出しにコンパイルされる。ならば、自作のクラスでもコレクション初期化子に対応できそうだ。

 実際には、Addメソッドを持っていることの他に、IEnumerableインタフェースを実装していることも条件になる(この2つの条件がMSDNにはOR条件であるように書かれているが、AND条件である)。

 コレクション初期化子に対応したクラス(=IEnumerableインタフェースを実装しAddメソッドを持つクラス)の例を次のコードに示す。

class SampleClass2 : IEnumerable
{
  private List<string> _strings = new List<string>();

  // Addメソッド(C# 6.0からは拡張メソッドでも可)
  public void Add(string item) => _strings.Add(item);

  public override string ToString() => string.Join(" ", _strings);

  // インタフェースIEnumerableの実装(IEnumerable<string>の実装でも可)
  IEnumerator IEnumerable.GetEnumerator() => _strings.GetEnumerator();
}

Class SampleClass2
  Implements IEnumerable

  Private _strings As List(Of String) = New List(Of String)

  ' Addメソッド(拡張メソッドでも可)
  Public Sub Add(item As String)
    _strings.Add(item)
  End Sub

  Public Overrides Function ToString() As String
    Return String.Join(" ", _strings)
  End Function

  ' インタフェースIEnumerableの実装(IEnumerable(Of String)の実装でも可)
  Private Function IEnumerable_GetEnumerator() As IEnumerator _
      Implements IEnumerable.GetEnumerator
    Return _strings.GetEnumerator()
  End Function
End Class

コレクション初期化子に対応したクラスの例(上:C#、下:VB)
IEnumerableインタフェースの実装とAddメソッドの他に、ToStringメソッドもオーバーライドした。ToStringメソッドでは、保持している文字列の全てを空白をはさんで繋ぎ合わせて返すようにしている。

 上の「SampleClass2」クラスを、コレクション初期化子を使って初期化するコードを次に示す。

WriteLine("IEnumerableでAddメソッドを持っていればよい");
SampleClass2 sc = new SampleClass2 { "Hello,", "Collection Initializer", "!!" };
WriteLine(sc);
// 出力:
// IEnumerableでAddメソッドを持っていればよい
// Hello, Collection Initializer !!

WriteLine("IEnumerableでAddメソッドを持っていればよい")
Dim sc As SampleClass2 _
  = New SampleClass2 From {"Hello,", "Collection Initializer", "!!"}
WriteLine(sc)
' 出力:
' IEnumerableでAddメソッドを持っていればよい
' Hello, Collection Initializer !!

コレクション初期化子に対応した自作クラスを使う例(上:C#、下:VB)

まとめ

 コレクション初期化子を使うと、コレクションのインスタンス化と要素の追加をいっぺんに書ける。配列初期化子と似ているが、VBではコレクション初期化子にFromキーワードが必須なので気を付けよう。

 コレクション初期化子の中でもオブジェクト初期化子は使える。VBでは、オブジェクト初期化子にWithキーワードが必要だ。

 コレクション初期化子に対応したクラスを作るには、IEnumerableインタフェースを実装し、Addメソッドを用意する。

「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。