解構元組和其他類型

元組可讓您輕鬆地從方法呼叫擷取多個值。 但擷取元組之後,您必須處理其個別項目。 逐元素執行這項作業很麻煩,如下列範例所示。 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# 不提供解構非元組型別和 recordDictionaryEntry 以外型別的內建支援。 不過,身為類別、結構或介面的作者,您可以藉由實作一或多個 Deconstruct 方法來解構類型的執行個體。 此方法會傳回 void,而且所要解構的每個值會以方法簽章中的 out 參數表示。 例如,Person 類別的下列 Deconstruct 方法會傳回名字、中間名和姓氏:

public void Deconstruct(out string fname, out string mname, out string lname)

您可以接著使用如下所示的程式碼指派,解構名為 pPerson 類別執行個體:

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 存取子的存取範圍是以 getAccesssetAccess 變數表示。

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 方法。 如需詳細資訊,請參閱屬性定義的位置語法衍生記錄中的解構函式行為

另請參閱