元组类型(C# 参考)

元组功能提供了简洁的语法来将多个数据元素分组成一个轻型数据结构。 下面的示例演示了如何声明元组变量、对它进行初始化并访问其数据成员:

(double, int) t1 = (4.5, 3);
Console.WriteLine($"Tuple with elements {t1.Item1} and {t1.Item2}.");
// Output:
// Tuple with elements 4.5 and 3.

(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
// Output:
// Sum of 3 elements is 4.5.

如前面的示例所示,若要定义元组类型,需要指定其所有数据成员的类型,或者,可以指定字段名称。 虽然不能在元组类型中定义方法,但可以使用 .NET 提供的方法,如下面的示例所示:

(double, int) t = (4.5, 3);
Console.WriteLine(t.ToString());
Console.WriteLine($"Hash code of {t} is {t.GetHashCode()}.");
// Output:
// (4.5, 3)
// Hash code of (4.5, 3) is 718460086.

元组类型支持相等运算符 ==!=。 有关详细信息,请参阅元组相等部分。

元组类型是值类型;元组元素是公共字段。 这使得元组为可变的值类型。

可以使用任意数量的元素定义元组:

var t =
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18,
19, 20, 21, 22, 23, 24, 25, 26);
Console.WriteLine(t.Item26);  // output: 26

元组的用例

元组最常见的用例之一是作为方法返回类型。 也就是说,你可以将方法结果分组为元组返回类型,而不是定义 out 方法参数,如以下示例所示:

int[] xs = new int[] { 4, 7, 9 };
var limits = FindMinMax(xs);
Console.WriteLine($"Limits of [{string.Join(" ", xs)}] are {limits.min} and {limits.max}");
// Output:
// Limits of [4 7 9] are 4 and 9

int[] ys = new int[] { -9, 0, 67, 100 };
var (minimum, maximum) = FindMinMax(ys);
Console.WriteLine($"Limits of [{string.Join(" ", ys)}] are {minimum} and {maximum}");
// Output:
// Limits of [-9 0 67 100] are -9 and 100

(int min, int max) FindMinMax(int[] input)
{
    if (input is null || input.Length == 0)
    {
        throw new ArgumentException("Cannot find minimum and maximum of a null or empty array.");
    }

    // Initialize min to MaxValue so every value in the input
    // is less than this initial value.
    var min = int.MaxValue;
    // Initialize max to MinValue so every value in the input
    // is greater than this initial value.
    var max = int.MinValue;
    foreach (var i in input)
    {
        if (i < min)
        {
            min = i;
        }
        if (i > max)
        {
            max = i;
        }
    }
    return (min, max);
}

如前面的示例所示,可以直接使用返回的元组实例,或者可以在单独的变量中析构它。

还可以使用元组类型,而不是匿名类型;例如,在 LINQ 查询中。 有关详细信息,请参阅在匿名类型和元组类型之间选择

通常,你会使用元组对相关的数据元素进行松散分组。 在公共 API 中,请考虑定义结构类型。

元组字段名称

可以在元组初始化表达式中或元组类型的定义中显式指定元组字段名称,如下面的示例所示:

var t = (Sum: 4.5, Count: 3);
Console.WriteLine($"Sum of {t.Count} elements is {t.Sum}.");

(double Sum, int Count) d = (4.5, 3);
Console.WriteLine($"Sum of {d.Count} elements is {d.Sum}.");

如果未指定字段名称,则可以根据元组初始化表达式中相应变量的名称推断出此名称,如下面的示例所示:

var sum = 4.5;
var count = 3;
var t = (sum, count);
Console.WriteLine($"Sum of {t.count} elements is {t.sum}.");

这称为元组投影初始值设定项。 在以下情况下,变量名称不会被投影到元组字段名称中:

  • 候选名称是元组类型的成员名称,例如 Item3ToStringRest
  • 候选名称是另一元组的显式或隐式字段名称的重复项。

在前面的示例中,你可以显式指定字段的名称,或按字段的默认名称访问字段。

元组字段的默认名称为 Item1Item2Item3 等。 始终可以使用字段的默认名称,即使字段名称是显式指定的或推断出的,如下面的示例所示:

var a = 1;
var t = (a, b: 2, 3);
Console.WriteLine($"The 1st element is {t.Item1} (same as {t.a}).");
Console.WriteLine($"The 2nd element is {t.Item2} (same as {t.b}).");
Console.WriteLine($"The 3rd element is {t.Item3}.");
// Output:
// The 1st element is 1 (same as 1).
// The 2nd element is 2 (same as 2).
// The 3rd element is 3.

元组赋值元组相等比较不会考虑字段名称。

在编译时,编译器会将非默认字段名称替换为相应的默认名称。 因此,显式指定或推断的字段名称在运行时不可用。

提示

启用 .NET 代码样式规则 IDE0037 以设置推理或显式元组字段名称的首选项。

从 C# 12 开始,可以使用 using 指令指定元组类型的别名。 以下示例为元组类型添加 global using 别名,其中包含表示允许的 Min 值和 Max 值的两个整数值:

global using BandPass = (int Min, int Max);

声明别名后,可以将 BandPass 名称用作该元组类型的别名:

BandPass bracket = (40, 100);
Console.WriteLine($"The bandpass filter is {bracket.Min} to {bracket.Max}");

别名不会引入新类型,而只会为现有类型创建同义词。 可以析构使用 BandPass 别名声明的元组,就像析构其基础元组类型一样:

(int a , int b) = bracket;
Console.WriteLine($"The bracket is {a} to {b}");

与元组赋值或析构一样,元组成员名称不需要进行匹配;类型需要进行匹配。

同样,具有相同 arity 和成员类型的第二个别名可以与原始别名互换使用。 可以声明第二个别名:

using Range = (int Minimum, int Maximum);

可以将 Range 元组分配给 BandPass 元组。 与所有元组赋值一样,字段名称不需要进行匹配,只有类型和 arity 需要进行匹配。

Range r = bracket;
Console.WriteLine($"The range is {r.Minimum} to {r.Maximum}");

使用元组时,元组类型的别名提供更多语义信息。 它不会引入新类型。 若要提供类型安全性,应改为声明位置 record

元组赋值和析构

C# 支持满足以下两个条件的元组类型之间的赋值:

  • 两个元组类型有相同数量的元素
  • 对于每个元组位置,右侧元组元素的类型与左侧相应的元组元素的类型相同或可以隐式转换为左侧相应的元组元素的类型

元组元素是按照元组元素的顺序赋值的。 元组字段的名称会被忽略且不会被赋值,如下面的示例所示:

(int, double) t1 = (17, 3.14);
(double First, double Second) t2 = (0.0, 1.0);
t2 = t1;
Console.WriteLine($"{nameof(t2)}: {t2.First} and {t2.Second}");
// Output:
// t2: 17 and 3.14

(double A, double B) t3 = (2.0, 3.0);
t3 = t2;
Console.WriteLine($"{nameof(t3)}: {t3.A} and {t3.B}");
// Output:
// t3: 17 and 3.14

还可以使用赋值运算符 = 在单独的变量中析构元组实例。 可以通过许多方式来进行此操作:

  • 在括号外使用 var 关键字来声明隐式类型化变量,并让编译器推断其类型:

    var t = ("post office", 3.6);
    var (destination, distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    
  • 在括号内显式声明每个变量的类型:

    var t = ("post office", 3.6);
    (string destination, double distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    
  • 在括号内显式声明一些类型,隐式声明其他类型(使用 var):

    var t = ("post office", 3.6);
    (var destination, double distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    
  • 使用现有变量:

    var destination = string.Empty;
    var distance = 0.0;
    
    var t = ("post office", 3.6);
    (destination, distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    

析构表达式的目标可以包括现有变量和析构声明中声明的变量。

还可以将析构与模式匹配相结合,以检查元组中字段的特征。 以下示例循环访问多个整数,并输出可被 3 整除的整数。 它析构 Int32.DivRem 的元组结果,并与 Remainder 0 进行匹配:

for (int i = 4; i < 20;  i++)
{
    if (Math.DivRem(i, 3) is ( Quotient: var q, Remainder: 0 ))
    {
        Console.WriteLine($"{i} is divisible by 3, with quotient {q}");
    }
}

有关析构元组和其他类型的详细信息,请参阅析构元组和其他类型

元组相等

元组类型支持 ==!= 运算符。 这些运算符按照元组元素的顺序将左侧操作数的成员与相应的右侧操作数的成员进行比较。

(int a, byte b) left = (5, 10);
(long a, int b) right = (5, 10);
Console.WriteLine(left == right);  // output: True
Console.WriteLine(left != right);  // output: False

var t1 = (A: 5, B: 10);
var t2 = (B: 5, A: 10);
Console.WriteLine(t1 == t2);  // output: True
Console.WriteLine(t1 != t2);  // output: False

如前面的示例所示,==!= 操作不会考虑元组字段名称。

同时满足以下两个条件时,两个元组可比较:

  • 两个元组具有相同数量的元素。 例如,如果 t1t2 具有不同数目的元素,t1 != t2 则不会进行编译。
  • 对于每个元组位置,可以使用 ==!= 运算符对左右侧元组操作数中的相应元素进行比较。 例如,(1, (2, 3)) == ((1, 2), 3) 不会进行编译,因为 1 不可与 (1, 2) 比较。

==!= 运算符将以短路方式对元组进行比较。 也就是说,一旦遇见一对不相等的元素或达到元组的末尾,操作将立即停止。 但是,在进行任何比较之前,将对所有元组元素进行计算,如以下示例所示:

Console.WriteLine((Display(1), Display(2)) == (Display(3), Display(4)));

int Display(int s)
{
    Console.WriteLine(s);
    return s;
}
// Output:
// 1
// 2
// 3
// 4
// False

元组作为 out 参数

通常,你会将具有 out 参数的方法重构为返回元组的方法。 但是,在某些情况下,out 参数可以是元组类型。 下面的示例演示了如何将元组作为 out 参数使用:

var limitsLookup = new Dictionary<int, (int Min, int Max)>()
{
    [2] = (4, 10),
    [4] = (10, 20),
    [6] = (0, 23)
};

if (limitsLookup.TryGetValue(4, out (int Min, int Max) limits))
{
    Console.WriteLine($"Found limits: min is {limits.Min}, max is {limits.Max}");
}
// Output:
// Found limits: min is 10, max is 20

元组与 System.Tuple

System.ValueTuple 类型支持的 C# 元组不同于 System.Tuple 类型表示的元组。 主要区别如下:

  • System.ValueTuple 类型是值类型System.Tuple 类型是引用类型
  • System.ValueTuple 类型是可变的。 System.Tuple 类型是不可变的。
  • System.ValueTuple 类型的数据成员是字段。 System.Tuple 类型的数据成员是属性。

C# 语言规范

有关详细信息,请参阅:

另请参阅