Tuple (Visual Basic)

從 Visual Basic 2017 開始,Visual Basic 語言提供 Tuple 的內建支援,讓建立 Tuple 和存取 Tuple 的項目更為輕鬆。 Tuple 是具有特定數目和值順序的輕量型資料結構。 當您具現化 Tuple 時,您會定義每個值 (或項目) 的數量和資料類型。 例如,2 Tuple (或配對) 具有兩個項目。 第一個可能是 Boolean 值,而第二個是 String。 由於 Tuple 可讓您輕鬆地將多個值儲存在單一物件中,因此通常用來作為從方法傳回多個值的輕量型方法。

重要

Tuple 支援需要 ValueTuple 類型。 如果未安裝 .NET Framework 4.7,您必須新增可在 NuGet 資源庫上使用的 NuGet 套件 System.ValueTuple。 如果沒有此套件,您可能會收到類似「未定義或匯入預先定義的類型 'ValueTuple(Of,,,)'」編譯錯誤。

具現化和使用 Tuple

您可以用括弧括住 Tuple 的逗號分隔值,以具現化 Tuple。 這些值接著會變成 Tuple 的欄位。 例如,下列程式碼會定義三個 Tuple (或 3 Tuple),並以 Date 作為其第一個值、String 作為第二個值,以及 Boolean 作為其第三個值。

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

根據預設,Tuple 中每個欄位的名稱是由字串 Item 所組成,以及該欄位在 Tuple 中以一為基底的位置。 針對這個 3 Tuple,Date 欄位為 Item1String 欄位為 Item2,而 Boolean 欄位為 Item3。 下列範例會顯示上一行程式碼中具現化 Tuple 的欄位值

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 Tuple 的欄位是讀寫的;具現化 Tuple 之後,您可以修改其值。 下列範例會修改上一個範例中所建立 Tuple 三個之中的兩個欄位,並顯示結果。

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

具現化和使用具名 Tuple

您可以藉由將自己的名稱指派給 Tuple 的項目,來具現化具名 Tuple,而非使用 Tuple 欄位的預設名稱。 然後,Tuple 的欄位可以透過其指派的名稱或預設名稱來存取。 下列範例會具現化與先前相同的 3 Tuple,不同之處在於其會明確命名第一個欄位 EventDate、第二個 Name 以及第三個 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

您也可以將 Tuple 名稱指定為變數、欄位或參數型別宣告的一部分:

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

或者,在方法的傳回型別中。

這在提供 Tuple 給集合初始設定式時特別有用;Tuple 名稱可作為集合型別宣告的一部分提供:

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

推斷的 Tuple 元素名稱

從 Visual Basic 15.3 開始,Visual Basic 可以推斷 Tuple 項目的名稱;您不需要明確指派名稱。 從一組變數初始化 Tuple,且您希望 Tuple 項目名稱與變數名稱相同時,推斷的 Tuple 名稱很有用。

下列範例會建立一個 stateInfo Tuple,其中包含三個明確的具名項目 statestateNamecapital。 請注意,在命名項目時,Tuple 初始化陳述式只會將相同具名變數的值指派給具名項目。

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

若要啟用推斷的 Tuple 項目名稱,您必須定義 Visual Basic 編譯器的版本,以在 Visual Basic 專案 (*.vbproj) 檔中使用:

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

版本號碼可以是從 15.3 開始的任何 Visual Basic 編譯器版本。 除了硬式編碼特定編譯器版本之外,您也可以將「Latest」指定為 LangVersion 的值,以安裝在您系統上的最新版 Visual Basic 編譯器進行編譯。

如需詳細資訊,請參閱設定 Visual Basic 語言版本

在某些情況下,Visual Basic 編譯器無法從候選名稱推斷 Tuple 項目名稱,且只能使用其預設名稱來參考 Tuple 欄位,例如 Item1Item2 等等。它們包括:

  • 候選名稱與 Tuple 成員的名稱相同,例如 Item3RestToString

  • 候選名稱在 Tuple 中重複。

當欄位名稱推斷失敗時,Visual Basic 不會產生編譯器錯誤,也不會在執行階段擲回例外狀況。 相反地,Tuple 欄位必須以其預先定義的名稱來參考,例如 Item1Item2

Tuple 與結構

Visual Basic Tuple 是一種實值型別,也就是其中一個 System.ValueTuple 泛型型別的執行個體。 例如,上一個範例中定義的 holiday Tuple 是 ValueTuple<T1,T2,T3> 結構的執行個體。 其設計目的是要成為資料的輕量型容器。 由於 Tuple 旨在讓您輕鬆地建立具有多個資料項目的物件,因此缺少自訂結構可能具有的某些功能。 包括:

  • 自訂成員。 您無法為 Tuple 定義自己的屬性、方法或事件。

  • 驗證。 您無法驗證指派給欄位的資料。

  • 永久性。 Visual Basic Tuple 是可變動的。 相反地,自訂結構可讓您控制執行個體是可變還是不可變的。

如果自訂成員、屬性和欄位驗證或不變性很重要,您應該使用 Visual Basic Structure 陳述式來定義自訂實值型別。

Visual Basic Tuple 會繼承其 ValueTuple 類型的成員。 除了其欄位之外,這些包括下列方法:

方法 描述
CompareTo 將目前的 Tuple 與具有相同項目數目的另一個 Tuple 進行比較。
Equals 判斷目前的 Tuple 是否等於另一個 Tuple 或物件。
GetHashCode 計算目前 執行個體的雜湊碼。
ToString 傳回此 Tuple 的字串表示,其格式為 (Item1, Item2...),其中 Item1Item2 代表 Tuple 欄位的值。

此外,ValueTuple 類型會實作 IStructuralComparableIStructuralEquatable 介面,讓您定義自訂比較子。

指派和 Tuple

Visual Basic 支援在具有相同欄位數目的 Tuple 類型之間進行指派。 如果下列其中一項成立,則可以轉換欄位類型:

  • 來源和目標欄位的類型相同。

  • 已定義來源類型到目標型別的放大 (或隱含) 轉換。

  • Option StrictOn,且已定義來源類型到目標型別的縮小 (或明確) 轉換。 如果來源值超出目標型別的範圍,此轉換可能會擲回例外狀況。

不會考慮對指派進行其他轉換。 讓我們看看 Tuple 類型之間允許的指派類型。

請考慮在下列範例中使用的這些參數:

' 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")

前兩個變數 (unnamedanonymous) 未提供欄位的語意名稱。 其欄位名稱是預設值 Item1Item2 。 最後兩個變數 (nameddifferentName) 具有語意欄位名稱。 請注意,這兩個 Tuple 有不同的欄位名稱。

所有這四個 Tuple 都有相同數目的欄位 (稱為 'arity'),且這些欄位的類型完全相同。 因此,所有這些指派都會運作︰

' 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

請注意,不會指派 Tuple 的名稱。 依欄位在 Tuple 中的順序,指派欄位的值。

最後,請注意,我們可以將 named Tuple 指派給 conversion Tuple,即使 named 的第一個欄位是 Integer,且 conversion 的第一個欄位是 Long。 此指派成功,因為將 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)

欄位數目不同的 Tuple 無法進行指派:

' 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

Tuple 作為方法傳回值

方法只能傳回單一值。 不過,您通常會想要方法呼叫來傳回多個值。 有數種方式可以解決這項限制:

  • 您可以建立自訂類別或結構,其屬性或欄位代表方法所傳回的值。 這是重量級的解決方案;其需要您定義自訂類型,其唯一目的是從方法呼叫擷取值。

  • 您可以從方法傳回單一值,並以傳址方式傳遞至方法以傳回剩餘值。 這牽涉到具現化變數的額外負荷,以及意外覆寫您以傳址方式所傳遞變數值的風險。

  • 您可以使用 Tuple,以提供輕量型解決方案來擷取多個傳回值。

例如,.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 方法的呼叫,我們可以從剖析作業傳回 Tuple。 在下列範例中,NumericLibrary.ParseInteger 會呼叫 Int32.TryParse 方法,並傳回具有兩個項目的具名 Tuple。

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

.NET Framework 中的 Visual Basic Tuple 和 Tuple

Visual Basic Tuple 是 .NET Framework 4.7 中所引進其中一個 System.ValueTuple 泛型型別的執行個體。 .NET Framework 也包含一組泛型 System.Tuple 類別。 不過,這些類別與 Visual Basic Tuple 和 System.ValueTuple 泛型型別有數種方式不同:

  • Tuple 類別的項目是名為 Item1Item2 等等的屬性。 在 Visual Basic Tuple 和 ValueTuple 類型中,Tuple 項目是欄位。

  • 您無法將有意義的名稱指派給 Tuple 執行個體或 ValueTuple 執行個體的項目。 Visual Basic 可讓您指派與傳達欄位意義的名稱。

  • Tuple 執行個體的屬性是唯讀的;而 Tuple 是不可變的。 在 Visual Basic Tuple 和 ValueTuple 型別中,Tuple 欄位是讀寫的;而 Tuple 是可變動的。

  • 泛型 Tuple 型別是參考型別。 使用這些 Tuple 型別表示配置物件。 在最忙碌路徑上,這可能會對您應用程式的效能造成可觀的衝擊。 Visual Basic Tuple 和 ValueTuple 型別是實值型別。

TupleExtensions 類別中的擴充方法可讓您輕鬆地在 Visual Basic Tuple 和 .NET Tuple 物件之間進行轉換。 ToTuple 方法會將 Visual Basic Tuple 轉換為 .NET Tuple 物件,而 ToValueTuple 方法會將 .NET Tuple 物件轉換為 Visual Basic Tuple。

下列範例會建立 Tuple、將其轉換成 .NET Tuple 物件,並將其轉換回 Visual Basic Tuple。 此範例接著會比較此 Tuple 與原始 Tuple,以確保兩者相等。

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

另請參閱