元組提供了一種輕量級的資料結構,能夠在單一結構中容納多個成員。 他們是比匿名類型更受青睞的選擇。 元組提供更高的效能、支援解構,並且具有更靈活的語法。
匿名類型提供一個便利的方法,將一組唯讀屬性封裝成一個物件,而不需要事先明確定義類型。 編譯器會產生型別名稱,且在原始碼層級無法取得。 編譯器會推斷每個屬性的類型。 主要在需要表達式樹支援或處理需要引用型別的程式碼時,使用匿名型別。
元組與匿名型別
無論是元組還是匿名型別,都可以讓你在不定義命名型別的情況下分組多個值。 然而,元組有更好的語言支援,且編譯後形成更高效的資料結構。 下表摘要說明重要差異:
| 特徵 / 功能 | 匿名型別 | Tuple |
|---|---|---|
| 類型 | 參考類型(class) |
價值類型(struct) |
| Performance | 記憶體堆分配 | 堆疊配置(效能更好) |
| Mutability | 唯讀屬性 | 可變場 |
| 解構 | 不支援 | 支持 |
| 表達式樹 | 支持 | 不支援 |
| 存取修飾符 | internal |
public |
| 成員名稱 | 必要或推斷 | 可選(預設名稱如 Item1) Item2 |
何時使用元組
在以下情況下使用元組:
- 你需要透過堆疊配置來提升效能。
- 你要把數值拆解成獨立的變數。
- 你從一個方法回傳多個值。
- 你不需要表達式樹的支援。
以下範例展示了元組如何以更乾淨的語法提供與匿名型態類似的功能:
// Tuple with named elements.
var tupleProduct = (Name: "Widget", Price: 19.99M);
Console.WriteLine($"Tuple: {tupleProduct.Name} costs ${tupleProduct.Price}");
// Equivalent example using anonymous types.
var anonymousProduct = new { Name = "Widget", Price = 19.99M };
Console.WriteLine($"Anonymous: {anonymousProduct.Name} costs ${anonymousProduct.Price}");
元組解構
你可以將元組拆解成獨立變數,這提供了處理個別元組元素的方便方式。 C# 支援多種拆解元組的方法:
static (string Name, int Age, string City) GetPersonInfo()
{
return ("Alice", 30, "Seattle");
}
// Deconstruct using var for all variables
var (name, age, city) = GetPersonInfo();
Console.WriteLine($"{name} is {age} years old and lives in {city}");
// Output: Alice is 30 years old and lives in Seattle
// Deconstruct with explicit types
(string personName, int personAge, string personCity) = GetPersonInfo();
Console.WriteLine($"{personName}, {personAge}, {personCity}");
// Deconstruct into existing variables
string existingName;
int existingAge;
string existingCity;
(existingName, existingAge, existingCity) = GetPersonInfo();
// Deconstruct and discard unwanted values using the discard pattern (_)
var (name2, _, city2) = GetPersonInfo();
Console.WriteLine($"{name2} lives in {city2}");
// Output: Alice lives in Seattle
解構在迴圈與模式匹配情境中非常有用:
var people = new List<(string Name, int Age)>
{
("Bob", 25),
("Carol", 35),
("Dave", 40)
};
foreach (var (personName2, personAge2) in people)
{
Console.WriteLine($"{personName2} is {personAge2} years old");
}
元組作為方法回傳類型
元組的常見使用情境是作為方法回傳型別。 你可以不去定義 out 參數,而是把方法結果分成元組。 你無法從方法回傳匿名型別,因為它沒有名稱,且回傳型別無法宣告。
以下範例示範使用帶有字典查詢的元組來回傳配置範圍:
var configLookup = new Dictionary<int, (int Min, int Max)>()
{
[2] = (4, 10),
[4] = (10, 20),
[6] = (0, 23)
};
if (configLookup.TryGetValue(4, out (int Min, int Max) range))
{
Console.WriteLine($"Found range: min is {range.Min}, max is {range.Max}");
}
// Output: Found range: min is 10, max is 20
此模式在處理需要同時回傳成功指標與多個結果值的方法時非常有用。 這個元組允許你使用命名欄位(Min和Max)取代像是Item1通用名稱(和Item2),使程式碼更易讀且具自我文件化能力。
何時使用匿名類型
在以下情況下使用匿名類型:
- 你正在處理表達式樹(例如,在某些 Microsoft 語言整合查詢(LINQ)提供者中)。
- 你需要物件為參考型別。
最常見的情況是使用其他類型的屬性來初始化匿名類型。 下列範例假設存在一個名為 Product 的類別。 類別 Product 包含 Color 和 Price 屬性,以及您不感興趣的其他屬性:
class Product
{
public string? Color { get; init; }
public decimal Price { get; init; }
public string? Name { get; init; }
public string? Category { get; init; }
public string? Size { get; init; }
}
匿名型別宣告以 new 運算元及 物件初始化器開始。 宣告會初始化新類型,只使用 Product 中的兩個屬性。 匿名型別通常用於 select 查詢表達式的子句中,以回傳較少的資料量。 如需查詢的詳細資訊,請參閱 LINQ in C#。
如果你在匿名型別中未指定成員名稱,編譯器會給匿名型態成員與初始化屬性相同的名稱。 您為以運算式初始化的屬性提供一個名稱,如上述範例所示。
在下列範例中,匿名類型的屬性名稱是 Color 和 Price。 這些實例是來自 products 以下 Product 類型集合中的項目:
var productQuery =
from prod in products
select new { prod.Color, prod.Price };
foreach (var v in productQuery)
{
Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price);
}
具有匿名型態的投影初始化器
匿名類型支援 投影初始化表達式,可讓您直接使用局部變數或參數,而不需要明確指定成員名稱。 編譯程式會從變數名稱推斷成員名稱。 下列範例示範這個簡化的語法:
// Explicit member names.
var personExplicit = new { FirstName = "Kyle", LastName = "Mit" };
// Projection initializers (inferred member names).
var firstName = "Kyle";
var lastName = "Mit";
var personInferred = new { firstName, lastName };
// Both create equivalent anonymous types with the same property names.
Console.WriteLine($"Explicit: {personExplicit.FirstName} {personExplicit.LastName}");
Console.WriteLine($"Inferred: {personInferred.firstName} {personInferred.lastName}");
這種簡化的語法在建立具有許多屬性的匿名類型時非常有用:
var title = "Software Engineer";
var department = "Engineering";
var salary = 75000;
// Using projection initializers.
var employee = new { title, department, salary };
// Equivalent to explicit syntax:
// var employee = new { title = title, department = department, salary = salary };
Console.WriteLine($"Title: {employee.title}, Department: {employee.department}, Salary: {employee.salary}");
在下列情況下,不會推斷成員名稱:
- 候選名稱在同一匿名類型中會與另一個屬性重複,不論是顯式或隱式。
- 候選名稱不是有效的識別碼(例如,它包含空格或特殊字元)。
在這些情況下,您必須明確指定成員名稱。
提示
您可以使用 .NET 樣式規則 IDE0037 來強制執行推斷或明確成員名稱。
你也可以用其他類型的物件來定義欄位:類別、結構體,甚至是其他匿名型態。 為此,請使用承載此物件的變數。 以下範例展示了兩種使用已實例化的使用者定義型別的匿名型態。 在這兩種情況下, product 匿名型別 shipment 和 shipmentWithBonus 的欄位都是型別 Product ,且包含每個欄位的預設值。 該 bonus 欄位屬於由編譯器建立的匿名類型。
var product = new Product();
var bonus = new { note = "You won!" };
var shipment = new { address = "Nowhere St.", product };
var shipmentWithBonus = new { address = "Somewhere St.", product, bonus };
一般而言,當您使用匿名型別初始化變數時,您可以使用 var 將變數宣告為隱含型別區域變數。 你無法在變數宣告中指定型別名稱,因為只有編譯器能存取匿名型別的底層名稱。 如需 var 的詳細資訊,請參閱隱含類型區域變數。
您可以如下列範例所示,合併隱含類型區域變數和隱含類型陣列,以建立匿名類型項目的陣列。
var anonArray = new[] { new { name = "apple", diam = 4 }, new { name = "grape", diam = 1 }};
匿名型別是從 class 直接繼承自 object 的型別,且除了 object 外,你無法將其轉型為任何其他型別。 編譯器會為每個匿名類型提供名稱,但您的應用程式無法存取它。 對 Common Language Runtime 來說,匿名類型與其他任何參考類型並無不同。
如果組件中有兩個或多個匿名物件初始設定式,指定了順序相同並具有相同名稱和類型的屬性序列,編譯器會將這些物件視為相同類型的執行個體。 這些物件會共用編譯器產生的相同類型資訊。
匿名型別會搭配運算式的形式支援非破壞性變化。 此功能允許您建立一個匿名類型的新實例,該實例中一個或多個屬性具有新值:
var apple = new { Item = "apples", Price = 1.35 };
var onSale = apple with { Price = 0.79 };
Console.WriteLine(apple);
Console.WriteLine(onSale);
您無法將欄位、屬性、事件或方法的傳回類型宣告為具有匿名類型。 同樣地,您無法將方法、屬性、建構函式或索引子的正式參數宣告為具有匿名類型。 若要以方法引數的形式來傳遞匿名類型或含有匿名類型的集合,您可以將參數宣告為 object 類型。 不過,針對匿名型別使用 object 會破壞強型別的目的。 如果您必須在方法界限外儲存或傳遞查詢結果,請考慮使用一般具名結構或類別來取代匿名類型。
由於匿名類型上的 Equals 和 GetHashCode 方法會以屬性的 Equals 和 GetHashCode 方法來定義,相同匿名類型的兩個執行個體僅在其所有屬性都相等時,這兩個執行個體才相等。
注意
匿名類型的 可及性層級 為 internal。 因此,在不同元件中定義的兩個匿名類型不屬於相同類型。
因此,當匿名型態在不同組合中定義時,即使所有性質相等,也無法彼此相等。
匿名型別會覆寫 ToString 方法,並串連以大括弧括住的每個屬性名稱和 ToString 輸出。
var v = new { Title = "Hello", Age = 24 };
Console.WriteLine(v.ToString()); // "{ Title = Hello, Age = 24 }"