タプル (Visual Basic)

Visual Basic 2017 以降、Visual Basic 言語ではタプルの組み込みサポートが提供されています。これによって、タプルの作成とタプルの要素へのアクセスが簡単になります。 タプルは、特定の数とシーケンスの値が含まれる軽量なデータ構造です。 タプルをインスタンス化するときは、各値 (または要素) の数とデータ型を定義します。 たとえば、2 タプル (つまりペア) には 2 つの要素があります。 1 つ目を Boolean 値、2つ目を String にすることができます。 タプルを使用すると複数の値を 1 つのオブジェクトに簡単に格納できるので、多くの場合、メソッドから複数の値を返すための軽量な方法として使用されます。

重要

タプルのサポートには ValueTuple 型が必要です。 .NET Framework 4.7 がインストールされていない場合は、NuGet ギャラリーから入手できる NuGet パッケージ System.ValueTuple を追加する必要があります。 このパッケージがないと、"定義済みの型 'ValueTuple(Of,,,)' は定義またはインポートされていません" のようなコンパイル エラーが発生することがあります。

タプルのインスタンス化と使用

タプルをインスタンス化するには、コンマ区切り値をかっこで囲みます。 これらの値のそれぞれがタプルのフィールドになります。 たとえば、次のコードではトリプル (つまり 3 タプル) が定義されており、Date が 1 つ目の値、String が 2 つ目の値、Boolean が 3 つ目の値です。

Dim holiday = (#07/04/2017#, "Independence Day", True)

既定では、タプルの各フィールドの名前は、文字列 Item と、タプル内のフィールドの位置を示す 1 から始まる数字で構成されます。 この 3 タプルでは、Date フィールドが Item1String フィールドが Item2Boolean フィールドが Item3 です。 次の例では、前のコード行でインスタンス化されたタプルのフィールドの値を表示します。

Console.WriteLine($"{holiday.Item1} is {holiday.Item2}" +
                  $"{If(holiday.Item3, ", a national holiday", String.Empty)}")
' Output: 7/4/2017 12:00:00 AM Is Independence Day, a national holiday

Visual Basic タプルのフィールドは読み取り/書き込み可能です。タプルをインスタンス化すると、その値を変更できます。 次の例では、前の例で作成したタプルの 3 つのフィールドのうち 2 つを変更し、結果を表示します。

holiday.Item1 = #01/01/2018#
holiday.Item2 = "New Year's Day"
Console.WriteLine($"{holiday.Item1} is {holiday.Item2}" +
                  $"{If(holiday.Item3, ", a national holiday", String.Empty)}")
' Output: 1/1/2018 12:00:00 AM Is New Year's Day, a national holiday

名前付きタプルのインスタンス化と使用

タプルのフィールドに既定の名前を使用するのではなく、独自の名前をタプルの要素に割り当てることによって、"名前付きタプル" をインスタンス化できます。 その後、タプルのフィールドに、割り当てられた名前 "または" 既定の名前でアクセスできます。 次の例では、前と同じ 3 タプルをインスタンス化していますが、1 つ目のフィールド EventDate、2 つ目 Name、3 つ目 IsHoliday を明示的に指定する点が異なります。 次に、フィールド値を表示し、変更してから、フィールドの値を再度表示します。

Dim holiday = (EventDate:=#07/04/2017#, Name:="Independence Day", IsHoliday:=True)
Console.WriteLine($"{holiday.EventDate} Is {holiday.Name}" +
                  $"{If(holiday.IsHoliday, ", a national holiday", String.Empty)}")
holiday.Item1 = #01/01/2018#
holiday.Item2 = "New Year's Day"
Console.WriteLine($"{holiday.Item1} is {holiday.Item2}" +
                  $"{If(holiday.Item3, ", a national holiday", String.Empty)}")
' The example displays the following output:
'   7/4/2017 12:00:00 AM Is Independence Day, a national holiday
'   1/1/2018 12:00:00 AM Is New Year's Day, a national holiday

変数、フィールド、またはパラメーターの型宣言の一部としてタプル名を指定することもできます。

Dim holiday As (EventDate As Date, Name As String, IsHoliday As Boolean) =
    (#07/04/2017#, "Independence Day", True)
Console.WriteLine(holiday.Name)
' Output: Independence Day

また、メソッドの戻り値の型に指定することもできます。

これは、コレクション初期化子にタプルを指定する場合に特に便利です。タプル名は、コレクションの型宣言の一部として指定できます。

Dim events As New List(Of (EventDate As Date, Name As String, IsHoliday As Boolean)) From {
    (#07/04/2017#, "Independence Day", True),
    (#04/22/2017#, "Earth Day", False)
}
Console.WriteLine(events(1).IsHoliday)
' Output: False

推論されたタプル要素の名前

Visual Basic 15.3 以降では、Visual Basic がタプル要素の名前を推定できます。明示的に割り当てる必要はありません。 推定タプル名は、一連の変数からタプルを初期化するときに、タプル要素名を変数名と同じにする場合に便利です。

次の例では、明示的に指定された3つの要素、statestateName、および capital を含む stateInfo タプルを作成します。 要素の名前を付けるとき、タプルの初期化ステートメントにより、名前が付けられる要素に同じ名前の変数の値が単に代入されることに注意してください。

Const state As String = "MI"
Const stateName As String = "Michigan"
Const capital As String = "Lansing"
Dim stateInfo = (state:=state, stateName:=stateName, capital:=capital)
Console.WriteLine($"{stateInfo.stateName}: 2-letter code: {stateInfo.state}, Capital {stateInfo.capital}")
' The example displays the following output:
'      Michigan: 2-letter code: MI, Capital Lansing

要素と変数の名前が同じであるため、次の例に示すように、Visual Basic コンパイラがフィールドの名前を推定できます。

Const state As String = "MI"
Const stateName As String = "Michigan"
Const capital As String = "Lansing"
Dim stateInfo = (state, stateName, capital)
Console.WriteLine($"{stateInfo.stateName}: 2-letter code: {stateInfo.State}, Capital {stateInfo.capital}")
' The example displays the following output:
'      Michigan: 2-letter code: MI, Capital Lansing

推定タプル要素名を有効にするには、Visual Basic プロジェクト (*.vbproj) ファイルに使用する Visual Basic コンパイラのバージョンを定義する必要があります。

<PropertyGroup>
  <LangVersion>15.3</LangVersion>
</PropertyGroup>

バージョン番号として、15.3 以降の任意の Visual Basic コンパイラ バージョンを指定できます。 特定のコンパイラ バージョンをハードコーディングする代わりに、LangVersion の値として "Latest" を指定すると、システムにインストールされている最新バージョンの Visual Basic コンパイラでコンパイルすることもできます。

詳細については、Visual Basic 言語バージョンの設定に関するページを参照してください。

場合によっては、Visual Basic コンパイラが候補名からタプル要素名を推定できないことがあり、Item1Item2 などの既定名を使用しないとタプル フィールドを参照できません。次の設定があります。

  • 候補名が、タプル メンバーの名前と同じです (Item3RestToString など)。

  • 候補名がタプル内で重複しています。

フィールド名の推論に失敗しても、Visual Basic はコンパイラ エラーを生成せず、実行時に例外がスローされることもありません。 代わりに、Item1Item2 など、定義済みの名前でタプル フィールドを参照する必要があります。

タプルと構造体

Visual Basic のタプルは、System.ValueTuple ジェネリック型の 1 つのインスタンスである値型です。 たとえば、前の例で定義されている holiday タプルは、ValueTuple<T1,T2,T3> 構造体のインスタンスです。 これは、データの軽量コンテナーとして設計されています。 タプルは、複数のデータ項目を含むオブジェクトを簡単に作成することを目的としているため、カスタム構造体であれば備えている機能の一部が欠落しています。 次の設定があります。

  • カスタム メンバー。 タプルに対して独自のプロパティ、メソッド、またはイベントを定義することはできません。

  • 検証。 フィールドに代入されたデータを検証することはできません。

  • 不変性。 Visual Basic のタプルは変更可能です。 対照的に、カスタム構造体では、インスタンスを変更可能にするかどうかを制御できます。

カスタム メンバー、プロパティとフィールドの検証、または不変性が重要な場合は、Visual Basic の Structure ステートメントを使用してカスタム値型を定義する必要があります。

Visual Basic のタプルは、ValueTuple 型のメンバーを継承します。 フィールドに加えて、次のメソッドが含まれています。

Method 説明
CompareTo 現在のタプルを、同じ数の要素を持つ別のタプルと比較します。
次の値に等しい 現在のタプルが別のタプルまたはオブジェクトと等しいかどうかを判断します。
GetHashCode 現在のインスタンスのハッシュ コードを計算します。
ToString (Item1, Item2...) という形式で、このタプルの文字列表現を返します。Item1Item2 はタプルのフィールドの値を表しています。

さらに、ValueTuple 型は IStructuralComparable および IStructuralEquatable インターフェイスを実装し、これらによってカスタム比較演算子を定義できます。

割り当てとタプル

Visual Basic では、同じ数のフィールドを含むタプル型の間の代入がサポートされます。 次のいずれかに該当する場合は、フィールドの型を変換できます。

  • 代入元と代入先のフィールドの型が同じです。

  • 代入元の型から代入先の型への拡大 (つまり暗黙) の変換が定義されています。

  • Option StrictOn になっており、代入元の型から代入先の型への縮小 (つまり明示的) の変換が定義されています。 代入元の値が代入先の型の範囲外の場合、この変換によって例外がスローされる可能性があります。

他の変換は、割り当てでは考慮されません。 タプル型間で許可されている割り当ての種類を見てみましょう。

以降の例で使用されている変数について考えます。

' The number and field types of all these tuples are compatible. 
' The only difference Is the field names being used.
Dim unnamed = (42, "The meaning of life")
Dim anonymous = (16, "a perfect square")
Dim named = (Answer:=42, Message:="The meaning of life")
Dim differentNamed = (SecretConstant:=42, Label:="The meaning of life")

最初の 2 つの変数 unnamed および anonymous では、フィールドにセマンティック名が割り当てられていません。 フィールド名は、既定の Item1Item2 です。 最後の 2 つの変数 named および differentName には、セマンティック フィールド名が付けられています。 この 2 つのタプルでは、フィールド名が異なっていることに注意してください。

この 4 つのタプルに含まれているフィールドの数 ("アリティ" と呼ばれます) とフィールドの型は同じです。 このため、これらの割り当てはすべて機能します。

' Assign named to unnamed.
named = unnamed

' Despite the assignment, named still has fields that can be referred to as 'answer' and 'message'.
Console.WriteLine($"{named.Answer}, {named.Message}")
' Output:  42, The meaning of life

' Assign unnamed to anonymous.
anonymous = unnamed
' Because of the assignment, the value of the elements of anonymous changed.
Console.WriteLine($"{anonymous.Item1}, {anonymous.Item2}")
' Output:   42, The meaning of life

' Assign one named tuple to the other.
named = differentNamed
' The field names are Not assigned. 'named' still has 'answer' and 'message' fields.
Console.WriteLine($"{named.Answer}, {named.Message}")
' Output:   42, The meaning of life

タプルの名前が割り当てられていないことに注意してください。 フィールドの値は、タプルのフィールドの順序に従って割り当てられます。

最終的に、named の 1 つ目のフィールドが Integer で、Long の 1 つ目のフィールドが conversion でも、named タプルを conversion タプルに代入できることに注意してください。 この代入が成功するのは、Integer から Long への変換が拡大変換であるためです。

' Assign an (Integer, String) tuple to a (Long, String) tuple (using implicit conversion).
Dim conversion As (Long, String) = named
Console.WriteLine($"{conversion.Item1} ({conversion.Item1.GetType().Name}), " +
                  $"{conversion.Item2} ({conversion.Item2.GetType().Name})")
' Output:      42 (Int64), The meaning of life (String)

フィールドの数が異なるタプルは代入できません。

' Does not compile.
' VB30311: Value of type '(Integer, Integer, Integer)' cannot be converted
'          to '(Answer As Integer, Message As String)'
var differentShape = (1, 2, 3)
named = differentShape

メソッドの戻り値としてのタプル

1 つのメソッドで返すことができる値は 1 つのみです。 しかし、1 回のメソッド呼び出しで複数の値を返すことが望ましい場合がよくあります。 この制限を回避するには、いくつかの方法があります。

  • カスタム クラスまたは構造体を作成して、そのプロパティまたはフィールドがメソッドによって返される値を表すようにできます。 これは、重量ソリューションです。メソッド呼び出しの値を取得するためだけにカスタム型を定義する必要があります。

  • メソッドから 1 つの値を返し、メソッドの参照によって残りの値を渡して返すことができます。 これは、変数をインスタンス化する際のオーバーヘッドが発生し、参照によって渡す変数の値が誤って上書きされるリスクがあります。

  • 複数の戻り値を取得するための軽量ソリューションを提供するタプルを使用できます。

たとえば、.NET の TryParse メソッドは、解析操作が成功したかどうかを示す Boolean 値を返します。 解析操作の結果は、メソッドへの参照によって渡される変数で返されます。 通常、Int32.TryParse などの解析メソッドを呼び出すと、次のようになります。

Dim numericString As String = "123456"
Dim number As Integer
Dim result = Integer.TryParse(numericString, number)
Console.WriteLine($"{If(result, $"Success: {number:N0}", "Failure")}")
'      Output: Success: 123,456

Int32.TryParse メソッドへの呼び出しを独自のメソッドでラップすると、解析操作からタプルを返すことができます。 次の例では、NumericLibrary.ParseIntegerInt32.TryParse メソッドを呼び出し、2 つの要素を含む名前付きタプルを返します。

Imports System.Globalization

Public Module NumericLibrary
    Public Function ParseInteger(value As String) As (Success As Boolean, Number As Integer)
        Dim number As Integer
        Return (Integer.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, number), number)
    End Function
End Module

さらに、次のコードを使用してメソッドを呼び出すことができます。

Dim numericString As String = "123,456"
Dim result = ParseInteger(numericString)
Console.WriteLine($"{If(result.Success, $"Success: {result.Number:N0}", "Failure")}")
Console.ReadLine()
'      Output: Success: 123,456

Visual Basic のタプルと .NET Framework のタプル

Visual Basic のタプルは、.NET Framework 4.7 で導入された System.ValueTuple ジェネリック型の 1 つのインスタンスです。 .NET Framework には、一連のジェネリック System.Tuple クラスも含まれています。 ただし、これらのクラスは、Visual Basic のタプルや System.ValueTuple ジェネリック型とはいくつかの点で異なっています。

  • Tuple クラスの要素は、Item1Item2 などと名前が付けられるプロパティです。 Visual Basic のタプルと ValueTuple 型では、タプルの要素はフィールドです。

  • Tuple インスタンスまたは ValueTuple インスタンスの要素には、わかりやすい名前を割り当てることはできません。 Visual Basic では、フィールドの意味を伝える名前を割り当てることができます。

  • Tuple インスタンスのプロパティは読み取り専用です。つまり、タプルは変更不可能です。 Visual Basic のタプルと ValueTuple 型では、タプルのフィールドは読み取り/書き込み可能です。つまり、タプルは変更可能です。

  • ジェネリック Tuple 型は参照型です。 これらの Tuple 型の使用は、オブジェクトの割り当てを意味します。 ホット パスでは、これがアプリケーションのパフォーマンスに大きな影響を及ぼすことがあります。 Visual Basic のタプルと ValueTuple 型は値型です。

TupleExtensions クラスの拡張メソッドを使用すると、Visual Basic のタプルと .NET Tuple オブジェクトの変換が簡単になります。 ToTuple メソッドは Visual Basic のタプルを .NET Tuple オブジェクトに変換し、ToValueTuple メソッドは .NET Tuple オブジェクトを Visual Basic のタプルに変換します。

次の例では、タプルを作成し、それを .NET Tuple オブジェクトに変換してから、再び Visual Basic のタプルに変換しています。 例は、その後、このタプルと最初のタプルを比較して、等しいことを確認しています。

Dim cityInfo = (name:="New York", area:=468.5, population:=8_550_405)
Console.WriteLine($"{cityInfo}, type {cityInfo.GetType().Name}")

' Convert the Visual Basic tuple to a .NET tuple.
Dim cityInfoT = TupleExtensions.ToTuple(cityInfo)
Console.WriteLine($"{cityInfoT}, type {cityInfoT.GetType().Name}")

' Convert the .NET tuple back to a Visual Basic tuple and ensure they are the same.
Dim cityInfo2 = TupleExtensions.ToValueTuple(cityInfoT)
Console.WriteLine($"{cityInfo2}, type {cityInfo2.GetType().Name}")
Console.WriteLine($"{NameOf(cityInfo)} = {NameOf(cityInfo2)}: {cityInfo.Equals(cityInfo2)}")

' The example displays the following output:
'       (New York, 468.5, 8550405), type ValueTuple`3
'       (New York, 468.5, 8550405), type Tuple`3
'       (New York, 468.5, 8550405), type ValueTuple`3
'       cityInfo = cityInfo2 :  True

関連項目