解構元組和其他類型
元組可讓您輕鬆地從方法呼叫擷取多個值。 但擷取元組之後,您必須處理其個別項目。 逐元素執行這項作業很麻煩,如下列範例所示。
QueryCityData
方法會傳回三元組,並以不同作業將其每個元素指派給變數。
public class Example
{
public static void Main()
{
var result = QueryCityData("New York City");
var city = result.Item1;
var pop = result.Item2;
var size = result.Item3;
// Do something with the data.
}
private static (string, int, double) QueryCityData(string name)
{
if (name == "New York City")
return (name, 8175133, 468.48);
return ("", 0, 0);
}
}
從一個物件擷取多個欄位和屬性值可能一樣麻煩:您必須逐成員將欄位或屬性值指派給個別變數。
您可以透過單一解構作業,從一個元組擷取多個元素,或從一個物件擷取多個欄位、屬性和計算值。 如要解構元組時,需將其元素指派給個別變數。 當您解構物件時,您會將選取的值指派給個別變數。
元組
C# 提供解構元組的內建支援,讓您以單一作業將元組中的所有項目解除封裝。 解構元組的一般語法類似定義元組的語法:您會在指派陳述式的左側,以括弧括住要指派每個項目的變數。 例如,下列陳述式會將四元組的元素指派給四個不同的變數:
var (name, address, city, zip) = contact.GetAddressInfo();
解構 Tuple 的方式有三種:
您可以明確地宣告括弧內每個欄位的類型。 下列範例使用此方法來解構
QueryCityData
方法傳回的三元組。public static void Main() { (string city, int population, double area) = QueryCityData("New York City"); // Do something with the data. }
您可以使用
var
關鍵字,讓 C# 推斷每個變數的類型。 請將var
關鍵字放在括弧外。 下列範例在解構QueryCityData
方法傳回的三元組時使用型別推斷。public static void Main() { var (city, population, area) = QueryCityData("New York City"); // Do something with the data. }
您也可以個別使用
var
關鍵字搭配括弧內的任何或所有變數宣告。public static void Main() { (string city, var population, var area) = QueryCityData("New York City"); // Do something with the data. }
上述範例很麻煩,不建議這麼做。
最後,您可以將 Tuple 解構為已宣告的變數。
public static void Main() { string city = "Raleigh"; int population = 458880; double area = 144.8; (city, population, area) = QueryCityData("New York City"); // Do something with the data. }
您可以在解構中混合變數宣告和指派。
public static void Main() { string city = "Raleigh"; int population = 458880; (city, population, double area) = QueryCityData("New York City"); // Do something with the data. }
您無法在括弧外指定特定型別,即使元組中的每個欄位都有相同的型別也一樣。 這樣做會產生編譯程式錯誤 CS8136:「解構 'var (...)' 窗體不允許 'var' 的特定類型。」
您必須將元組的每個元素指派給個別變數。 如果您省略任何元素,編譯器會產生錯誤 CS8132:「無法將 'x' 元素的元組解構為 'y' 變數。」
具有 Discard 的元組元素
解構元組時,您通常只對部分項目的值感興趣。 您可以利用 C# 對 捨棄的支援,這是您選擇忽略其值的唯寫變數。 您在指派中宣告具有底線字元 (“_”) 的捨棄。 您可以視需要捨棄多個值;單一捨棄 _
代表所有捨棄的值。
下列範例說明如何搭配使用元組與 discard。
QueryCityDataForYears
方法會傳回六元組,包含城市名稱、區域、年份、該年度城市人口、次年度年份、次年度城市人口這些資料。 此範例會顯示這兩年之間的人口變化。 在元組可用的資料中,我們對城市區碼不感興趣,並在設計階段得知城市名稱及兩個日期。 因此,我們只對元組中所儲存的兩個人口值感興趣,而可以將其餘值視為 discard。
using System;
public class ExampleDiscard
{
public static void Main()
{
var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);
Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
}
private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
{
int population1 = 0, population2 = 0;
double area = 0;
if (name == "New York City")
{
area = 468.48;
if (year1 == 1960)
{
population1 = 7781984;
}
if (year2 == 2010)
{
population2 = 8175133;
}
return (name, area, year1, population1, year2, population2);
}
return ("", 0, 0, 0, 0, 0);
}
}
// The example displays the following output:
// Population change, 1960 to 2010: 393,149
使用者定義型別
C# 提供解構元組類型、record
和 DictionaryEntry 類型的內建支援。 不過,身為類別、結構或介面的作者,您可以藉由實作一或多個 Deconstruct
方法來解構類型的執行個體。 方法會傳回“void”。 方法簽章中的 out 參數代表要解構的每個值。 例如,下列 Person
類別的 Deconstruct
方法會傳回名字、中間名和姓:
public void Deconstruct(out string fname, out string mname, out string lname)
您可以接著使用如下所示的程式碼指派,解構名為 Person
的 p
類別執行個體:
var (fName, mName, lName) = p;
下列範例會多載 Deconstruct
方法,以傳回 Person
物件屬性的各種組合。 每個多載會傳回:
- 名字和姓氏。
- 名字、中間名和姓。
- 名字、姓氏、城市名稱和州名。
using System;
public class Person
{
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public string City { get; set; }
public string State { get; set; }
public Person(string fname, string mname, string lname,
string cityName, string stateName)
{
FirstName = fname;
MiddleName = mname;
LastName = lname;
City = cityName;
State = stateName;
}
// Return the first and last name.
public void Deconstruct(out string fname, out string lname)
{
fname = FirstName;
lname = LastName;
}
public void Deconstruct(out string fname, out string mname, out string lname)
{
fname = FirstName;
mname = MiddleName;
lname = LastName;
}
public void Deconstruct(out string fname, out string lname,
out string city, out string state)
{
fname = FirstName;
lname = LastName;
city = City;
state = State;
}
}
public class ExampleClassDeconstruction
{
public static void Main()
{
var p = new Person("John", "Quincy", "Adams", "Boston", "MA");
// Deconstruct the person object.
var (fName, lName, city, state) = p;
Console.WriteLine($"Hello {fName} {lName} of {city}, {state}!");
}
}
// The example displays the following output:
// Hello John Adams of Boston, MA!
具有相同數量參數的多個 Deconstruct
方法模棱兩可。 您必須小心定義具有不同數量參數 (也稱為「arity」) 的 Deconstruct
方法。 在多載解析期間,無法區分具有相同數目參數的 Deconstruct
方法。
具有 Discard 的使用者定義型別
就像元組一樣,您可以使用 discard 來忽略 Deconstruct
方法傳回的選取項目。 名為 “_” 的變數代表捨棄。 單一解構作業可以包含多個棄置。
下列範例會將 Person
物件解構為四個字串(名字和姓氏、城市和州),但捨棄家族名稱和州。
// Deconstruct the person object.
var (fName, _, city, _) = p;
Console.WriteLine($"Hello {fName} of {city}!");
// The example displays the following output:
// Hello John of Boston!
解構擴充方法
如果您並未撰寫類別、結構或介面,仍然可以解構該類型的物件,方法是實作一或多個 Deconstruct
擴充方法來傳回您想要的值。
下列範例為 Deconstruct
類別定義兩個 System.Reflection.PropertyInfo 擴充方法。 第一個會傳回一組值,指出 屬性的特性。 第二個方法指出屬性的存取範圍。 布爾值表示屬性是否有個別的 get 和 set 存取子或不同的輔助功能。 如果只有一個存取子,或是 get 和 set 存取子具有相同的存取權限,則 access
變數會指出屬性的整個存取權限。 否則,get 和 set 存取子的存取範圍是以 getAccess
和 setAccess
變數表示。
using System;
using System.Collections.Generic;
using System.Reflection;
public static class ReflectionExtensions
{
public static void Deconstruct(this PropertyInfo p, out bool isStatic,
out bool isReadOnly, out bool isIndexed,
out Type propertyType)
{
var getter = p.GetMethod;
// Is the property read-only?
isReadOnly = ! p.CanWrite;
// Is the property instance or static?
isStatic = getter.IsStatic;
// Is the property indexed?
isIndexed = p.GetIndexParameters().Length > 0;
// Get the property type.
propertyType = p.PropertyType;
}
public static void Deconstruct(this PropertyInfo p, out bool hasGetAndSet,
out bool sameAccess, out string access,
out string getAccess, out string setAccess)
{
hasGetAndSet = sameAccess = false;
string getAccessTemp = null;
string setAccessTemp = null;
MethodInfo getter = null;
if (p.CanRead)
getter = p.GetMethod;
MethodInfo setter = null;
if (p.CanWrite)
setter = p.SetMethod;
if (setter != null && getter != null)
hasGetAndSet = true;
if (getter != null)
{
if (getter.IsPublic)
getAccessTemp = "public";
else if (getter.IsPrivate)
getAccessTemp = "private";
else if (getter.IsAssembly)
getAccessTemp = "internal";
else if (getter.IsFamily)
getAccessTemp = "protected";
else if (getter.IsFamilyOrAssembly)
getAccessTemp = "protected internal";
}
if (setter != null)
{
if (setter.IsPublic)
setAccessTemp = "public";
else if (setter.IsPrivate)
setAccessTemp = "private";
else if (setter.IsAssembly)
setAccessTemp = "internal";
else if (setter.IsFamily)
setAccessTemp = "protected";
else if (setter.IsFamilyOrAssembly)
setAccessTemp = "protected internal";
}
// Are the accessibility of the getter and setter the same?
if (setAccessTemp == getAccessTemp)
{
sameAccess = true;
access = getAccessTemp;
getAccess = setAccess = String.Empty;
}
else
{
access = null;
getAccess = getAccessTemp;
setAccess = setAccessTemp;
}
}
}
public class ExampleExtension
{
public static void Main()
{
Type dateType = typeof(DateTime);
PropertyInfo prop = dateType.GetProperty("Now");
var (isStatic, isRO, isIndexed, propType) = prop;
Console.WriteLine($"\nThe {dateType.FullName}.{prop.Name} property:");
Console.WriteLine($" PropertyType: {propType.Name}");
Console.WriteLine($" Static: {isStatic}");
Console.WriteLine($" Read-only: {isRO}");
Console.WriteLine($" Indexed: {isIndexed}");
Type listType = typeof(List<>);
prop = listType.GetProperty("Item",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
var (hasGetAndSet, sameAccess, accessibility, getAccessibility, setAccessibility) = prop;
Console.Write($"\nAccessibility of the {listType.FullName}.{prop.Name} property: ");
if (!hasGetAndSet | sameAccess)
{
Console.WriteLine(accessibility);
}
else
{
Console.WriteLine($"\n The get accessor: {getAccessibility}");
Console.WriteLine($" The set accessor: {setAccessibility}");
}
}
}
// The example displays the following output:
// The System.DateTime.Now property:
// PropertyType: DateTime
// Static: True
// Read-only: True
// Indexed: False
//
// Accessibility of the System.Collections.Generic.List`1.Item property: public
系統型別的擴充方法
為求方便,某些系統型別會提供 Deconstruct
方法。 例如,System.Collections.Generic.KeyValuePair<TKey,TValue> 型別提供這項功能。 當您遍歷 System.Collections.Generic.Dictionary<TKey,TValue>時,每個元素都是 KeyValuePair<TKey, TValue>
,而且可以拆解。 請考慮下列範例:
Dictionary<string, int> snapshotCommitMap = new(StringComparer.OrdinalIgnoreCase)
{
["https://github.com/dotnet/docs"] = 16_465,
["https://github.com/dotnet/runtime"] = 114_223,
["https://github.com/dotnet/installer"] = 22_436,
["https://github.com/dotnet/roslyn"] = 79_484,
["https://github.com/dotnet/aspnetcore"] = 48_386
};
foreach (var (repo, commitCount) in snapshotCommitMap)
{
Console.WriteLine(
$"The {repo} repository had {commitCount:N0} commits as of November 10th, 2021.");
}
record
型別
當您使用兩個或多個位置參數宣告 record 型別時,編譯器會為 Deconstruct
宣告中的每個位置參數建立一個具有 out
參數的 record
方法。 如需詳細資訊,請參閱屬性定義的位置語法和衍生記錄中的解構函式行為。