元组 (Visual Basic)

从 Visual Basic 2017 开始,Visual Basic 语言为元组提供内置支持,使创建元组和访问元组的元素变得更容易。 元组是一种轻型数据结构,具有特定数目和序列的值。 实例化元组时,需要定义每个值(或元素)的数量和数据类型。 例如,二元组(或对)具有 2 个元素。 第一个可能是 Boolean 值,而第二个可能是 String。 通过元组可在单个对象中轻松地存储多个值,因此它们通常用作从方法中返回多个值的一种轻量化方法。

重要

元组支持需要 ValueTuple 类型。 如果未安装 .NET Framework 4.7,则必须添加 NuGet 包 System.ValueTuple(可在 NuGet 库中获取)。 如果没有此包,可能会收到类似于“未定义或导入预定义类型 "ValueTuple(Of,,,)"”的编译错误。

实例化和使用元组

将元组以逗号分隔的值括在括号中来实例化元组。 然后,每个值将成为一个元组字段。 例如,以下代码定义了三元组(3 元组),它的第一个值是 Date,第二个值是 String,第三个值是 Boolean

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 元组的字段是读写的;实例化元组后,可修改其值。 以下示例修改在上一示例中创建的元组的两个字段(共三个)并显示结果。

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 元组,只不过它将第一个字段显式命名为 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

还可将元组名称指定为变量、字段或参数的类型声明的一部分:

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 可推断元组元素的名称;你不需要显式分配它们。 如果从一组变量初始化元组,并且希望元组元素名称与变量名称相同,那么推断得到的元组名称非常有用。

以下示例创建一个 stateInfo 元组,其中包含三个显式命名的元素:statestateNamecapital。 请注意,在命名元素时,元组初始化语句只是为命名元素分配同名变量的值。

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 编译器的版本,以在 Visual Basic 项目 (*.vbproj) 文件中使用:

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

版本号可以是 Visual Basic 编译器自 15.3 起的任何版本。 还可指定“最新”作为 LangVersion 的值,以使用安装在系统上的最新版本的 Visual Basic 编译器进行编译,而不是对特定编译器版本进行硬编码。

有关详细信息,请参阅设置 Visual Basic 语言版本

在某些情况下,Visual Basic 编译器无法从候选名称推断元组元素名称,并且只能使用默认名称(例如 Item1Item2等)引用元组字段。其中包括:

  • 候选名称与元组成员的名称相同,例如 Item3RestToString

  • 候选名称在元组中重复。

如果字段名称推理失败,Visual Basic 不会生成编译器错误,也不会在运行时引发异常。 相反,元组字段必须由其预定义的名称(例如 Item1Item2)进行引用。

元组与结构

Visual Basic 元组是一个值类型,它是某个 System.ValueTuple 泛型类型的实例。 例如,上一示例中定义的 holiday 元组是 ValueTuple<T1,T2,T3> 结构的实例。 它设计为数据的轻型容器。 元组旨在让你轻松创建具有多个数据项的对象,因此它缺少自定义结构可能具有的一些功能。 其中包括:

  • 自定义成员。 不能为元组定义自己的属性、方法或事件。

  • 验证。 无法验证分配给字段的数据。

  • 不可变性。 Visual Basic 元组是可变的。 相反,通过自定义结构可控制实例是可变的还是不可变的。

如果自定义成员、属性和字段验证,或者不可变性很重要,则应使用 Visual Basic Structure 语句来定义自定义值类型。

Visual Basic 元组会继承其 ValueTuple 类型的成员。 除了字段外,还包括以下方法:

方法 描述
CompareTo 将当前元组与具有相同数量的元素的另一个元组进行比较。
等于 确定当前元组是否等于另一个元组或对象。
GetHashCode 计算当前实例的哈希代码。
ToString 返回此元组的字符串表示形式,它采用 (Item1, Item2...)形式,其中 Item1Item2 表示元组字段的值。

此外,ValueTuple 类型实现 IStructuralComparableIStructuralEquatable 接口,这使你能够定义自定义比较器。

赋值和元组

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

前两个变量(unnamedanonymous)没有为字段提供语义名称。 其字段名称是默认的 Item1Item2。 最后两个变量(nameddifferentName)具有语义字段名称。 请注意,这两个元组具有不同的字段名称。

这四个元组具有相同数量的字段(称为“实参数量”),这些字段的类型也完全一样。 因此可进行以下赋值:

' 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 元组分配给 conversion 元组,即使 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)

具有不同数量的字段的元组不可分配:

' 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

作为方法返回值的元组

一个方法只能返回一个值。 但是,你通常希望一个方法调用返回多个值。 有几种方法可绕过此限制:

  • 可创建自定义类或结构,其属性或字段表示方法返回的值。 这是一个重量级解决方案;它要求定义一个自定义类型,该类型的唯一用途是从方法调用中检索值。

  • 可从方法返回单个值,并通过引用方法传递剩余值来返回这些值。 这涉及到变量实例化开销,还可能会无意中覆盖通过引用传递的变量的值。

  • 可使用提供用于检索多个返回值的轻量级解决方案的元组。

例如,.NET 中的 TryParse 方法会返回一个 Boolean 值,该值指示分析操作是否成功。 分析操作的结果在通过引用方法传递的变量中返回。 通常,调用 Integer.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

如果我们用自己的方法封装对 Integer.TryParse 方法的调用,就可以从解析操作中返回一个元组。 在下面的示例中,NumericLibrary.ParseInteger 调用 Integer.TryParse 方法,并返回一个包含两个元素的已命名元组。

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 元组是某个 System.ValueTuple 泛型类型的实例,这些类型已在 .NET Framework 4.7 中引入。 .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

请参阅