解構元組和其他類型
元組可讓您輕鬆地從方法呼叫擷取多個值。 但擷取元組之後,您必須處理其個別項目。 逐元素執行這項作業很麻煩,如下列範例所示。 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. }
從 C# 10 開始,您可以在解構作業中混用變數宣告和指派。
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 支援,這是您已選擇忽略其值的唯寫變數。 Discard 是由指派中的底線字元 ("_") 選擇。 您可以視需要捨棄許多值,每個值都會以單一 discard _
表示。
下列範例說明如何搭配使用元組與 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)
您可以接著使用如下所示的程式碼指派,解構名為 p
的 Person
類別執行個體:
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
方法傳回的選取項目。 每個 discard 是由名為 "_" 的變數定義,而單一解構作業可包含多個 discard。
下列範例會將 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
擴充方法來傳回您想要的值。
下列範例為 System.Reflection.PropertyInfo 類別定義兩個 Deconstruct
擴充方法。 第一個方法會傳回一組值,指出屬性的特性 (包括其類型)、這是靜態或執行個體、是否為唯讀,以及是否已建立索引。 第二個方法指出屬性的存取範圍。 因為 get 和 set 存取子的存取範圍可能不同,所以布林值會指出屬性是否有不同的 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.");
}
您可以將 Deconstruct
方法新增至沒有此方法的系統型別。 請考慮下列擴充方法:
public static class NullableExtensions
{
public static void Deconstruct<T>(
this T? nullable,
out bool hasValue,
out T value) where T : struct
{
hasValue = nullable.HasValue;
value = nullable.GetValueOrDefault();
}
}
此擴充方法可讓所有 Nullable<T> 型別解構為 (bool hasValue, T value)
的元組。 下列範例顯示使用此擴充方法的程式碼:
DateTime? questionableDateTime = default;
var (hasValue, value) = questionableDateTime;
Console.WriteLine(
$"{{ HasValue = {hasValue}, Value = {value} }}");
questionableDateTime = DateTime.Now;
(hasValue, value) = questionableDateTime;
Console.WriteLine(
$"{{ HasValue = {hasValue}, Value = {value} }}");
// Example outputs:
// { HasValue = False, Value = 1/1/0001 12:00:00 AM }
// { HasValue = True, Value = 11/10/2021 6:11:45 PM }
record
型別
當您使用兩個或多個位置參數宣告 record 型別時,編譯器會為 record
宣告中的每個位置參數建立一個具有 out
參數的 Deconstruct
方法。 如需詳細資訊,請參閱屬性定義的位置語法和衍生記錄中的解構函式行為。
另請參閱
意見反應
https://aka.ms/ContentUserFeedback。
即將登場:在 2024 年,我們將逐步淘汰 GitHub 問題作為內容的意見反應機制,並將它取代為新的意見反應系統。 如需詳細資訊,請參閱:提交並檢視相關的意見反應