元組與解構

小提示

剛開始開發軟體嗎? 先從 入門 教學開始。 當你需要從某個方法回傳多個值或組合值,但又不想定義命名型別時,就會遇到元組。

有其他語言的經驗嗎? C# 元組是類似 Python 或 Swift 元組的值型別,但具備可選的命名元素及完整解構支援。 略讀 解構等式 部分,尋找 C# 專用的模式。

元組將多個值分組成一個輕量級結構,無需定義具名型別。 元組是值型別,你可以內聯宣告、從方法回傳,並拆解成個別變數。 當你需要快速、暫時地將相關值分組時,可以使用元組。 例如,當你從一個方法回傳多個結果或儲存一對座標時。

以下範例建立一個包含命名元素的元組,並以名稱存取每個元素:

var location = (Latitude: 47.6062, Longitude: -122.3321);
Console.WriteLine($"Location: {location.Latitude}, {location.Longitude}");
// Output: Location: 47.6062, -122.3321

元組對於短命的群組很有效,因為定義類別、結構或記錄會增加不必要的儀式。 對於具有行為的長壽命領域概念或類型,偏好 記錄類別結構。 關於何時使用每種類型,請參見 「選擇哪種類型」。

宣告並初始化元組

宣告元組時,請在括號內列出元素類型。 你可以選擇為每個元素命名,讓程式碼更易讀:

// Tuple with named elements
(string Name, int Age) person = ("Alice", 30);
Console.WriteLine($"{person.Name} is {person.Age} years old");

// Tuple with default element names (Item1, Item2)
(string, int) unnamed = ("Bob", 25);
Console.WriteLine($"{unnamed.Item1} is {unnamed.Item2} years old");

// Tuple declared with var and inline names
var city = (Name: "Seattle", Population: 749_256);
Console.WriteLine($"{city.Name}: population {city.Population}");

當你不提供名稱時,元素會使用預設名稱 Item1、 , Item2等等。 命名元素讓你的程式碼能自我文件化,不需要另外的型別定義。

推斷元素名稱

編譯器會從你用來初始化元組的變數名稱或屬性名稱推斷元素名稱。 此功能避免了名稱相符時重複:

var name = "Carol";
var age = 28;

// The compiler infers element names from the variable names
var person = (name, age);
Console.WriteLine($"{person.name} is {person.age}");
// Output: Carol is 28

推斷名稱能讓你的程式碼保持簡潔。 如果你需要不同的元素名稱,請明確指定。

從一個方法回傳多個值

元組最常見的用途之一是從一個方法回傳多個值。 與其定義類別或使用 out 參數,不如回傳一個帶有命名元素的元組:

static (double Minimum, double Maximum, double Average) ComputeStats(List<double> values)
{
    var min = values.Min();
    var max = values.Max();
    var avg = values.Average();
    return (min, max, avg);
}

命名元組的元素使返回值在呼叫點和方法簽名上更具可讀性。 呼叫者可以依名稱存取每個值,無需記住位置順序。

解構元組

解構 將元組的元素拆包成單一語句中的獨立變數。 你可以用幾種方式拆解元組:

var point = (X: 3, Y: 7);

// Deconstruct with var (infer all types)
var (x, y) = point;
Console.WriteLine($"x={x}, y={y}");

// Deconstruct with explicit types
(int px, int py) = point;
Console.WriteLine($"px={px}, py={py}");

// Deconstruct into existing variables
int a, b;
(a, b) = point;
Console.WriteLine($"a={a}, b={b}");

// Deconstruct a method return value directly
List<double> data = [10.0, 20.0, 30.0];
var (min, max, avg) = ComputeStats(data);
Console.WriteLine($"Min: {min}, Max: {max}, Avg: {avg}");

當你透過方法呼叫收到元組,並需要立即處理其個別值時,解構特別有用。

你可以直接在 foreach 迴圈中拆解元組,這使得對分組值的組合進行迭代變得簡潔:

List<(string Name, int Score)> results =
[
    ("Alice", 92),
    ("Bob", 87),
    ("Carol", 95)
];

foreach (var (name, score) in results)
{
    Console.WriteLine($"{name}: {score}");
}

當你不需要每個元素時,可以用 丟棄_)來替代你想忽略的每個值。 為每個丟棄位置單獨使用一個 _

List<double> values = [5.0, 10.0, 15.0];
var (_, max, _) = ComputeStats(values);
Console.WriteLine($"Only need the max: {max}");
// Output: Only need the max: 15

關於在不同情境中使用棄牌的更多資訊,請參見 棄牌

元組相等

你可以使用 ==!= 比較元組。 這些操作符會依序比較每個元素,因此當兩個元組的所有對應元素相等時,兩個元組是相等的。

var order1 = (Product: "Widget", Quantity: 5);
var order2 = (Product: "Widget", Quantity: 5);
var order3 = (Product: "Gadget", Quantity: 3);

Console.WriteLine(order1 == order2); // True
Console.WriteLine(order1 == order3); // False

// Element names don't affect equality—only values matter
var named = (X: 1, Y: 2);
var different = (A: 1, B: 2);
Console.WriteLine(named == different); // True

元組等式使用每個元素類型上定義的 == 運算子,這意味著對於字串、數字及定義了 == 的其他類型,比較是正確的。 元素名稱不影響等值——只有數值和位置才重要。

非破壞性突變 with

with 表達式會建立一個元組的複製品,並改變一個或多個元素,保留原始元素不變:

var original = (Name: "Widget", Price: 19.99m, InStock: true);
var discounted = original with { Price = 14.99m };

Console.WriteLine($"Original: {original.Name} at {original.Price:C}");
Console.WriteLine($"Discounted: {discounted.Name} at {discounted.Price:C}");
// Output:
// Original: Widget at $19.99
// Discounted: Widget at $14.99

這個模式在你想要現有元組的變體而不修改原始元組時非常有用。 這個 with 表達式在元組和 記錄上的運作方式相同。

字典和查找操作中的元組

當你需要將鍵與多個資料片段關聯時,元組是方便的字典值:

var sizeChart = new Dictionary<string, (int Min, int Max)>
{
    ["Small"] = (0, 50),
    ["Medium"] = (51, 100),
    ["Large"] = (101, 200)
};

if (sizeChart.TryGetValue("Medium", out var range))
{
    Console.WriteLine($"Medium: {range.Min}–{range.Max}");
}
// Output: Medium: 51–100

元組也可以當作字典 ,提供一個複合鍵,無需定義自訂型別。 由於元組實作結構等同,查找會根據所有元素的合併值相匹配:

var grid = new Dictionary<(int Row, int Column), string>
{
    [(0, 0)] = "Origin",
    [(1, 3)] = "Sensor A",
    [(2, 5)] = "Sensor B"
};

var target = (Row: 1, Column: 3);
if (grid.TryGetValue(target, out var label))
{
    Console.WriteLine($"({target.Row}, {target.Column}): {label}");
}
// Output: (1, 3): Sensor A

此模式避免了為簡單的多鍵查詢或鍵到多值映射而需要獨立類別。

元組 vs. 匿名型別

當你需要輕量級無名資料結構時,元組是首選。 匿名型別仍可用於表達式樹情境及需要參考型別的程式碼,但元組提供更佳的效能、解構支援及更靈活的語法。 關於匿名型態的更多資訊,請參見 「匿名型別與元組型別的選擇」。

另請參閱