基本的な本能
Visual Basic 2010 のコレクション初期化子と配列初期化子
Adrian Spotty Bowles
Microsoft Visual Studio (VS) 2010 製品開発サイクル中に実施された言語の変更の一部は、これまで多くのスケルトン コードを使用しなければ実現できなかったタスクを簡素化することを目的としています。Visual Studio 2010 で行われたこうしたアプローチは、コーディング エクスペリエンスを改善し、生産性を向上します。Visual Studio 2010 リリースの新機能の 1 つに、コレクション初期化子があります。この機能は 2008 の C# で提供されたもので、全般的に機能差をなくすというマイクロソフトの取り組みの一環として提供されています。Visual Basic 2010 の変更の中には、すぐにはわかりにくいものもあります。今回のコラムでは、このように比較的わかりにくい実装の詳細をいくつか説明します。
コレクションと配列は、最近のアプリケーションでは非常によく使用される構成要素ですが、こうしたコレクションは多くの場合初期化しなければなりません。2010 より前にリリースされた Visual Basic では、制限がありながらも基本的な配列初期化子を扱うことができましたが、コレクションを同様の 1 行のステートメントで初期化することはできなかったため、よく次のようなスケルトン コードが使用されていました。
Dim x As New List(Of String)
x.Add("Item1")
x.Add("Item2")
または、クラスのコンストラクターを作成し、その中で次のようなコードを使用する必要がありました。
Dim x as new list(of string)({"Item1","Item2"})
どちらの方法も、特に洗練されたソリューションというわけではありませんでした。
どちらも機能はしますが、コレクションを初期化するために追加のコードを作成する必要がありました。こうしたコードは、たいてい簡単なスケルトン コードでしたが、ソース コードが増えて、メンテナンス コストの発生につながる可能性がありました。
コレクションを使用するコードのほぼすべてで、同様のスケルトン コードが使用されることになります。上記の例では、2 つの文字列要素のみを追加し、それぞれの文字列に対して Add メソッドを繰り返し呼び出しています。カスタム "ユーザー定義" コレクションまたは "ユーザー定義" 型のコレクションを使用すると、同じコレクションの初期化タスクを実行するための標準的に必要なコードの量は増えるでしょう。
新しい構文
Visual Studio 2010 では、複数のコード行と Add メソッドの繰り返し呼び出しを次のような 1 行のステートメントにまとめ、コレクションの初期化タスクがかなり簡潔に処理できるようになりました。
Dim x As New List(Of String) From {"Item1", "Item2"}
これは、フレームワークの汎用リスト コレクション型を使用して、タイプセーフなコレクションを提供する簡単な例です。これらのコレクション型は、これまでタイプセーフなコレクションの実装に使用されてきたユーザー定義のコレクション型よりも、広く使用されるようになっています。
この構文では、項目のリストを中かっこ { } で囲んでコレクション メンバーを指定できます。このとき、各項目はコンマで区切り、"FROM" キーワードを使用して初期メンバー リストを定義します。前の例は、リスト型には有効ですが、各メンバーにキーと値のペアが指定されるディクショナリ型など、より複雑なコレクション型の場合には、各キーと値のペアをさらに中かっこで囲む必要があります。
Dim x As New Dictionary(Of Integer, String)
From {{1, "Item1"}, {2, "Item2"}}
この構文は明快で一貫性があります。各キーと値のペアは入れ子になった {} により特定され、コレクション型 (Dictionary) の Add メソッドが呼び出されるときに、これらの引数が使用されます。先ほどのコード行は、次のコードと同等です。
Dim x As New Dictionary(Of Integer, String)
x.Add(1, "Item1")
x.Add(2, "Item2")
他のコレクション型の実装と用法
前の例では、一般的なフレームワーク型に使用する場合のコレクションの初期化方法を説明しています。ただし、独自のコレクション型を実装している場合や、Add メソッドがないためにすぐに初期化子を使用できない型 (Stack や Queue など) を使用する場合があります。
これらのシナリオでもコレクション初期化子機能を使用できるようにするために、背後でどのような処理が行われているかと、この機能がどのように実装されているかについて、少し理解しておくことが重要です。これを理解しておくと、簡単なリストやディクショナリの例以外でも、この機能を使用できるようになります。
コレクション初期化子の構文を使用するには、2 つの要件が満たされる必要があります。具体的には、型は次の状態でなければなりません。
- IEnumerable パターンを実装する。これは、IEnumerable インターフェイスか、単に GetEnumerator メソッドを含むかのいずれかです。"ダック" タイピングと呼ばれることもあります。つまり、「アヒルのように見え、アヒルのように鳴くのであれば、それはおそらくアヒルである」という考えです。
この例では、「適切なシグネチャの GetEnumerator メソッドを含んでいるなら、それはおそらく IEnumerable インターフェイスと同様の動作を実装する」と見なします。このダック タイピングの動作は、型を For Each 構造で使用できるようにするために既に使用されています。 - 1 つのパラメーターを受け取る、アクセス可能な Add メソッドを少なくとも 1 つ含む。これは、インスタンスか拡張メソッドのいずれかです。
コレクション クラスが上記の両方の条件を満たせば、コレクション初期化子の構文を使用できます。
コレクション初期化子の場合、実際にはコレクションの初期化に IEnumerable メソッドを使用しませんが、この型が実際にコレクション型であると判断されるヒントとして、IEnumerable メソッドを使用します。IEnumerable または IEnumerable パターンを実装すると、保証はされませんが、コレクションである可能性が高いと見なされます。
Add メソッドが、初期化子リスト内の項目ごとに呼び出されて、その項目がコレクションに追加されます。Add メソッドがあるというだけで、コレクションに項目を追加する機能を実装しなければならないとは限りません。しかし、引数を "初期化子リスト" の項目に入れ替えて Add メソッドが呼び出されます。
この点を示すには次のコード サンプルが有効です (図 1 参照)。ここではコレクション初期化子構文を使用できますが、実際に項目をコレクションに追加していません。
その他のフレームワーク クラス - 拡張メソッドとの併用
フレームワーク コレクション型の中には、上記の両方の要件を満たさないものもあります。Stack や Queue は、Add ではなく Pop や Push などのメソッドを実装します。このような型に簡潔なコレクション初期化子機能を使用できるようにするには、拡張メソッドの力を利用します。これらの型の Add 拡張メソッドを作成するのは簡単です。このメソッドを作成すると、簡単な構文でメソッドを初期化できるようになります。
Imports System.Runtime.CompilerServices
Module Module1
Sub Main()
Dim st1 As New Stack(Of Integer) From {1, 2, 3}
End Sub
End Module
Module Extensions
<Extension()> Sub Add(Of t)(ByVal x As Stack(Of t), ByVal y As t)
x.Push(y)
End Sub
End Module
拡張メソッドを使用するときは、通常、制御できる型のみを拡張するようにします。拡張メソッドを乱用すると、型がアップグレードされる場合に、動作の競合や変更が発生する可能性があるため注意してください。
暗黙の行継続との併用
Visual Studio 2010 には、また別の待望の機能であった行継続が含まれています。この機能を使用すると、こうるさい _ 文字を排除できます。暗黙の行継続は "初期化リスト" 内の項目にも使用できるため、わかりやすくなるように各項目を 1 行ずつ記述できます。
Dim st1 As New Stack(Of Integer) From {1,
2,
3}
図 1 コレクション初期化子構文
Module Module1
Sub Main()
Dim NC As New TestNonCollectionClass From {1, 2, 3}
End Sub
End Module
Class TestNonCollectionClass
Public Function GetEnumerator() As System.Collections.IEnumerator
'Does Nothing
End Function
Public Sub Add(ByVal x As Integer)
If x = 1 Then
Console.WriteLine("Add Item" & x.ToString)
End If
End Sub
End Class
これにより、読みやすく、余分な _ 文字を含まず、繰り返しがない明快なコードを作成できます。これは、独自のコレクションにも、既にフレームワークに用意されているコレクションにも使用できます。エディターの IntelliSense サポートのおかげで、前述の 2 つの要件に従うすべての型に "FROM" がサポートされるため、カスタム コレクション型にも使用できます。_ 文字を使用する既存のコードがあり、古い構文との整合性を優先する場合は、古い構文を使用することもできます。
例外処理の動作
"初期化子リスト" のメンバーごとに Add メソッドを呼び出す場合に、指摘しておくべき興味深い点がいくつかあります。
たとえば、初期化リストから渡される引数を指定して Add メソッドを呼び出した場合に例外が発生すると、リストは初期化されずどのメンバーも設定されません。つまり、実際の動作としては、コレクションはすべてのメンバーで初期化されるか、まったく初期化されないかのいずれかです。
つまり、リストに 3 項目あり、3 番目の項目を指定して Add メソッドを呼び出したときに例外が発生した場合、例外がスローされ、コレクションは初期化されていない状態のままになります。次の例では、意図的に例外を生成して、まさにこのシナリオを実際に発生させ、コンソールに "Blank" と出力されるようにしています。ただし、初期化子リストを変更して 3 番目の値を削除すると、"Initialized:Element Count 2" が返されます (図 2 参照)。
図 2 初期化子リストの変更
Imports System.Runtime.CompilerServices
Module Module1
Sub Main()
Dim l1 As Stack(Of Integer)
Try
l1 = New Stack(Of Integer) From {1, 2}
Catch ex As Exception
End Try
If l1 Is Nothing Then
Console.WriteLine("Blank")
Else
Console.WriteLine("Initialized - Element Count:" & l1.Count)
End If
End Sub
End Module
Module Extensions
<Extension()> Sub Add(Of t)(ByVal x As Stack(Of t), ByVal y As t)
If y.ToString = "3" Then
Throw New Exception("Intentional Exception")
Else
x.Push(y)
End If
End Sub
End Module
この状況が発生しなかった場合は、追加のコードを作成して初期化例外が発生していないかどうか、またコレクションがどのような状態かを特定する必要があります。
拡張メソッドによる構文の短縮
複雑なオブジェクトのコレクションを初期化する標準の構文では、初期化子リストの項目ごとに "New <型>" を繰り返す必要があります。
Module Module1
Sub Main()
Dim CustomerList As New List(Of Customer)
From {New Customer With {.Name = "Spotty", .Age = 39},
New Customer With {.Name = "Karen", .Age = 37}}
End Sub
End Module
Class Customer
Public Property Name As String = ""
Public Property Age As Integer = 0
End Class
この構文では、初期化子リストの各項目のインスタンスが作成される必要があります。しかし、拡張メソッドを使用すると、この構文をさらに短くすることができます。この機能は、コレクション型が IEnumerable インターフェイスをサポートすると同時に Add メソッドも保持しているためで、かっこで囲まれている初期化子リストの項目が Add メソッドのパラメーターにマップされます (図 3 参照)。
図 3 拡張メソッドの使用
Imports System.Runtime.CompilerServices
Module Module1
Sub Main()
'Shortend Syntax through the use of extension methods
Dim CustomerList As New List(Of Customer)
From {{"Spotty", 39}, {"Karen", 37}}
End Sub
End Module
Module Extension
<Extension()> Public Sub add(ByVal x As List(Of Customer), _
ByVal Name As String, ByVal Age As Integer)
x.add(New Customer With {.Name = Name, .Age = Age})
End Sub
End Module
Class Customer
Public Property Name As String = ""
Public Property Age As Integer = 0
End Class
Add メソッドのオーバーロード
Add メソッドについての条件はこの機能にとって不可欠で、オーバーロードできます。各メソッドは、呼び出し引数を基に独自のオーバーロードを呼び出します。魔法などではなく、どのメソッドでもオーバーロードの解決が機能するように、Add メソッドでもオーバーロードの解決が機能します。
これも、簡単な例によって実例を示すのが一番です。データ型ごとに複数のオーバーロードがあるとします (図 4 参照)。ここでは、呼び出し引数に適したものが呼び出されます。
構文で '=' ではなく 'FROM' が使用される理由
この機能について製品チームによく寄せられる質問の 1 つは "FROM" キーワードの使用に関してですが、このキーワードを使用しているのは配列初期化子が既に "=" を代入に使用しているためです。
Visual Basic では、3 とおりの方法でコレクション クラスのインスタンスを作成できます。
Dim x1 As New List(Of Integer)
Dim x2 = New List(Of Integer)
Dim x3 As List(Of Integer) = New List(Of Integer)
コレクションのインスタンスを作成する構文のうち 2 つは、既に構文内に = 文字を使用しています。その上さらに = 文字を使用すると、構文の中でコレクション型の新しいインスタンスの代入に使われるものと、コレクションのメンバーを初期化するものとが紛らわしくなるでしょう。
= 文字ではなくキーワードを使用することで、コレクション初期化子の操作の結果が確定されるため、この問題を回避でき、すべての既存の構文規則でコレクション型を宣言および初期化できます。それでも、初期化子リストを定義する方法は、既存の配列初期化子の構文ではおなじみものです。
多くの話し合いの結果、FROM キーワードが選ばれました。LINQ クエリをよくご存知であれば、LINQ で既に FROM キーワードが使用されているため、おかしな選択だと思われるかもしれません。この選択は、LINQ クエリ内で使用された場合はあいまいになる可能性があるように見えますが、実際にはそうなりません。これについては、次に説明します。
クエリ内では 'FROM' と 'FROM' が競合しないか
コレクション初期化子を含むクエリであっても、キーワードはあいまいになりません。これは、次のコードで実例を示すことができます。
Module Module1
Sub Main()
'The first from is the query,
'the second is the collection initializer.
'The parser can always successfully identify the difference
Dim x = From i In New List(Of Integer) From {1, 2, 3, 4}
Where i <= 3
Select i
For Each i In x
Console.WriteLine(i)
Next
Stop
End Sub
End Module
コレクション初期化子とオブジェクト初期化子とを併用する場合の制限事項
Visual Basic チームは 2008 バージョンで、オブジェクトのインスタンスを作成するときに、オブジェクトのフィールドやプロパティを初期化できるように、オブジェクト初期化子を実装しました。
Dim x As New List(Of Integer) With {.Capacity = 10}
ただし、同じ宣言でオブジェクトとコレクションの両方を初期化できないため、次のコードでは構文エラーが発生します。
Dim x as New List(Of Integer) from {1,2,3} With {.Capacity = 10}
他にも、バージョン 2010 では配列関連の比較的目立たない変更がいくつかあります。この変更はすぐには気付かれなくても、配列を使用するコードに影響する可能性があります。
図 4 異なるデータ型用のオーバーロード
Imports System.Runtime.CompilerServices
Module Module1
Sub Main()
Dim l1 As New Stack(Of Integer) From {1, 2.2, "3"}
Console.WriteLine("Element Count:" & l1.Count)
Stop
End Sub
End Module
Module Extensions
<Extension()> Sub Add(ByVal X1 As Stack(Of Integer), _
ByVal x2 As Integer)
Console.WriteLine("Integer Add")
X1.Push(x2)
End Sub
<Extension()> Sub Add(ByVal X1 As Stack(Of Integer), _
ByVal x2 As Double)
Console.WriteLine("Double Add")
X1.Push(CInt(x2))
End Sub
<Extension()> Sub Add(ByVal X1 As Stack(Of Integer), _
ByVal x2 As String)
Console.WriteLine("String Add")
X1.Push(CInt(x2))
End Sub
End Module
配列初期化子の変更点
バージョン 2010 より前は、簡単な配列は初期化できましたが、それでもいくつか制限事項がありました。最近の変更の 1 つにより、さらに簡潔な方法で、配列型を初期化および推論できるようにしています。
これまでは、Visual Basic で項目を配列として識別するには、識別子に 1 組のかっこを指定する必要がありました。
Dim a() As Integer = {1, 2, 3}
この 1 組のかっこが不要になり、Option Infer On により型が配列型として推論されるようになったため、より簡潔な構文で表されるようになりました。
Dim a = {1, 2, 3}
配列型の推論
バージョン 2008 で型の推論機能が実装されました。これは、データ型をその代入値から推論する機能です。これは、メソッド本体内で宣言されている単一オブジェクトでは機能しますが、配列では型を推論できなかったため、常にオブジェクト型の配列として扱われました。
Sub Main()
Dim a() = {1, 2, 3}
'Type would previously result in Object Array prior to 2010
End Sub
これが改善され、次のように配列型も推論されるようになりました。
Sub Main()
Dim a = {1, 2, 3} 'Type now infers Integer Array
End Sub
推論される配列の型は、2008 で追加された優先度の高い型によって決まります。この型推論は、メソッド本体内で宣言されているコードでしか機能しないことに変わりはないため、フィールドは明示的に型が指定されていない限り、やはりオブジェクト配列になります。
複数の配列次元を初期化することはできますが、入れ子にされたすべてのペアは、同じ数のメンバーを保持していなければなりません。したがって、2 次元の例は機能します。
次の 2 つの例は、どちらも 2 次元の整数配列を作成します。
Dim TwoDimension1(,) = {{1, 2}, {3, 4}}
Dim TwoDimension2 = {{1, 2}, {3, 4}}
1 次元配列の場合、構文は上記のコードと非常に似ており、いくつか例外はありますが、すべての既存のコードは引き続き機能します。
Dim a() = {1, 2, 3}
上記のステートメントでは、配列型が初期化子リストから推論されます。結果として、整数配列が推論されます。以前は、初期化子リストから配列型を推論しないため、これらは単にオブジェクト配列になりました。型をチェックするコードを使用している場合、リフレクションを使用する場合、またはオブジェクト配列型を含め複数のオーバーロードを保持する場合、現在は異なる結果になります。この例には、遅延バインディング メソッドがあります。
多次元配列
多次元配列を初期化するには、次元数と初期化子リスト内の項目数が一致していなければなりません。次元数と項目数が同じではなく、初期化リスト内で入れ子になっているすべての項目で項目数が同じでない場合、構文エラーが発生します。
Dim TwoDimension2 = {{1, 2}, {3, 4}}
'Valid 2 dimension integer(,) array inferred
Dim TwoDimension2Invalid(,) = {{1}, {3}}
'Invalid – dimension and element count mismatch
Dim TwoDimension2Invalid1(,) = {{1, 2}, {3}}
'Invalid:element count not consistent in Initializer List
つまり、前と同様の構文を使用して、標準の固定次元配列を初期化できます。
ジャグ配列の初期化
ただし、これはジャグ配列 (配列の配列) の場合、この構文はそのままでは機能しないことを意味します。ジャグ配列で初期化子を使用できるようにするには、各メンバー リストをかっこで囲む必要があります。
Sub Main()
'Incorrect Jagged Array syntax
Dim JaggedDimensionIncorrect()() = {{1,2},{3,4,5}}
'Correct Jagged Array syntax
Dim JaggedDimension1()() = {({1,2}),({3,4,5})}
End Sub
上記の例は、次のような結果になります。
- JaggedDimension1(0) は要素 1,2 が設定された整数配列を保持する
- JaggedDimension1(1) は要素 3,4,5 が設定された整数配列を保持する
配列型の推論は、入れ子になった配列型でも機能します。もう少し複雑な例を紹介します。
Dim JaggedDimension1() = {({1, 2}), ({3.1, 4.2, 5.3})}
この場合は、JaggedDimension1 が Object() であると推論され、メンバーは型 Integer() と Double() であると推論されます。
配列の推論と予期される動作
そこで、初期化子リストを推論された配列だと思ったら、大間違いです。初期化子リストは、誤解されているかもしれませんが、具象型ではありません。これは、メンバー リストの構文的表現で、使用されるコンテキストに応じて特定の型になります。
以下のコード サンプルでは、{, , ,} 構文を使用して、以下の作業が可能であることを示しています。
1. 整数配列の推論と初期化:
Dim a = {1, 2, 3} 'Infers Integer Array
これは、ターゲットの型が指定されておらず、初期化子リストのメンバーの優先度の高い型を基に型が推論されるため機能します。
2. 異なる型の配列の初期化:
Dim b As Single() = {1, 2, 3}
'Will convert each integer value into single
これは、値 1、2、および 3 を使用して single 型の配列として初期化することで機能します。初期化子リスト自体には、組み込み型はありません。
ただし、次のコードは、失敗すると思われます。
Dim a = {1, 2, 3} 'Infers Integer()
Dim c As Single() =
a 'No conversion between Single() and Integer()
少し奇妙に思われるかも知れませんが、変数 a は整数型の配列として推論され、値 1、2、および 3 を使用して初期化されます。変数 c は Single 型の配列として宣言されています。ただし、Single 型配列と整数配列間の変換がないため、構文エラーが発生します。
配列初期化子の単独使用
配列初期化子リストは単独でも使用できます。この場合、特定のターゲット型は持たず、初期化リストのメンバーの優先度の高い型の配列として機能します。
これにより、今までは実現できなかった興味深いシナリオがいくつか可能になります。たとえば、データ構造の初期化に使用する項目の配列を受け取るメソッドを作成できます。この場合、呼び出されたメソッドが、呼び出し引数として使用されるさまざまな既定値を判断します (図 5 参照)。
このような場合、呼び出しのたびにローカル配列を作成し、単純に配列型をメソッドに渡します。しかし、この新しい単独使用機能により、このようなシナリオで不要なローカル配列を作成しなくてもよくなり、単独の初期化子リストを使用して配列型を渡すことができます。
初期化子リストを単独使用シナリオで使用する場合、配列型の推論が非常に簡単です。したがって、単独使用シナリオでは、初期化リストは配列リテラルと捉えることもできます。これにより、個々のメソッド呼び出しなど、限られた場合にしか使用されない項目を宣言する必要がなくなります。
図 5 呼び出し引数として使用される複数の既定値
Module Module1
Sub InitializeMethod(ByVal x As String())
'....
End Sub
Sub Main()
Dim i As Integer = 1
Select Case I
Case 1
Dim Array1 As String() = {"Item1", "Item2"}
InitializeMethod(Array1)
Case 2
Dim Array1 As String() = {"Item11", "Item12"}
InitializeMethod(Array1)
End Select
End Sub
End Module
2010 のその他の機能との併用
Visual Studio 2010 の機能の 1 つに、複数バージョンの製品への対応があります。この機能はバージョン 2008 にも存在していましたが、Visual Studio 2010 の言語機能の多くを、下位バージョン対応 (2.0、3.0、および 3.5 対応) にできるようにするための機能強化がいくつか施されています。したがって、Visual Studio 2010 を使用すれば、この強化機能を既存のアプリケーションのコレクション初期化子や配列初期化子に使用できます。これにより、既存のソース コードを簡素化すると同時に、新しい言語機能を利用できます。
では、この新機能を使用するときに、既存のコードが動作しなくなる可能性はないのでしょうか。その可能性はあるというのが答えで、下位互換性の問題が発生する可能性がいくぶんあります。しかし、これはわずかなシナリオに限られ、元のコードが動作しなくなることよりもメリットの方が大きく、既存の機能は容易に維持できます。
Dim x1() = {1, 2, 3}
以前のバージョンでは、上記のコードはオブジェクトの配列になりますが、Visual Studio 2010 では、要素の型のうち優先度の高い型に推論されるため Integer() になります。この動作は、対象バージョンが 4.0 であっても、それ以前のバージョンであっても変わりません。ただし、コードで特定の型のオブジェクト配列を想定している場合は失敗する可能性があります。これは、対象とする型を明示的に指定することで簡単に解決できます。
Dim x1() As Object = {1, 2, 3}
この動作の例外は、要素に優先度の高い型がない場合です。
Dim x1() = {1, 2.2, "test"}
この場合は以前の動作が有効になり、x1 はオブジェクトの配列になります。
応用は簡単
コレクション初期化子は、Visual Basic にとってすばらしい追加機能です。フレームワークのコレクション型を初期化する場合も、ユーザー定義のコレクション型を初期化する場合も、はるかに簡潔な構文を使用できます。また、最小限の変更で、この構文を既存のコードに容易に応用できます。
この機能は、拡張メソッドなど他の言語機能とも併用できるように最適化されていて、コレクション型に使用する構文を削減できます。このように以前使用されていたスケルトン コードを削減できるため、コードのサイズが小さくなり、メンテナンスが容易になります。
配列初期化子の動作はわずかに変更され、配列型の推論が実装されと機能が強化されています。また、配列の単独使用が可能になり、前のバージョンの多次元配列やジャグ配列を初期化できるように機能強化されました。
事実上すべてのアプリケーションが配列やコレクションを使用しているため、この機能はほぼすべてのプロジェクトで使用できるでしょう。
Adrian Spotty Bowles は、すべてのバージョンの Visual Basic による開発を経て、現在はワシントン州レドモンドで、Visual Basic 製品チームのソフトウェア設計エンジニア兼テスターとして、Visual Basic コンパイラを担当しています。Visual Basic 2008 リリースでは、拡張メソッドを含むさまざまな言語機能を担当しました。連絡先は Abowles@microsoft.com です。