析构元组和其他类型

元组提供一种从方法调用中检索多个值的轻量级方法。 但是,一旦检索到元组,就必须处理它的各个元素。 按元素逐个操作比较麻烦,如下例所示。 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# 提供内置的元组析构支持,可在单个操作中解包一个元组中的所有项。 用于析构元组的常规语法与用于定义元组的语法相似:将要向其分配元素的变量放在赋值语句左侧的括号中。 例如,以下语句将四元组的元素分配给 4 个单独的变量:

var (name, address, city, zip) = contact.GetAddressInfo();

有三种方法可用于析构元组:

  • 可以在括号内显式声明每个字段的类型。 以下示例使用此方法来析构由 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.
    }
    

    前面的示例很繁琐,不建议这样做。

  • 最后,可以将元组解构为已声明的变量。

    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" 变量”。

使用弃元的元组元素

析构元组时,通常只需要关注某些元素的值。 可利用 C# 对弃元的支持,弃元是一种仅能写入的变量,且其值将被忽略。 在赋值中使用下划线字符(“_”)声明一个弃元。 您可以丢弃任意数量的值,一个丢弃标记 _表示所有已丢弃的值。

以下示例演示了对元组使用弃元时的用法。 QueryCityDataForYears 方法返回一个六元组,包含城市名称、城市面积、一个年份、该年份的城市人口、另一个年份及该年份的城市人口。 该示例显示了两个年份之间人口的变化。 对于元组提供的数据,我们不关注城市面积,并在一开始就知道城市名称和两个日期。 因此,我们只关注存储在元组中的两个人口数量值,可将其余值作为占位符处理。

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)

然后,可使用与下列代码类似的赋值来析构名为 Personp 类的实例:

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 方法是不明确的。 在定义 Deconstruct 方法时,必须小心使用不同数量的参数或“arity”。 在重载解析过程中,不能区分具有相同数量参数的 Deconstruct 方法。

使用弃元的用户定义类型

就像使用元组一样,可使用弃元来忽略 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 访问器的可访问性由 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.");
}

record 类型

使用两个或多个位置参数声明记录类型时,编译器将为 Deconstruct 声明中的每个位置参数创建一个带有 out 参数的 record 方法。 有关详细信息,请参阅属性定义的位置语法派生记录中的解构函数行为

请参阅