元組型別 (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 = [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 = [-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 指示詞來指定元組型別的別名。 下列範例會針對允許 MinMax 值新增具有兩個整數值元組型別的 global using 別名:

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

如同元組指派或解構,元組成員名稱不需要相符;型別則需要。

同樣地,具有相同元數和成員型別的第二個別名可以與原始別名互換使用。 您可以宣告第二個別名:

using Range = (int Minimum, int Maximum);

您可以將 Range 元組指派給 BandPass 元組。 如同所有元組指派,欄位名稱不需要相符,只有型別和元數。

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 的元組結果,並比對 0 的 Remainder

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# 語言規格

如需詳細資訊,請參閱

另請參閱