析构元组和其他类型Deconstructing tuples and other types

元组提供一种从方法调用中检索多个值的轻量级方法。A tuple provides a lightweight way to retrieve multiple values from a method call. 但是,一旦检索到元组,就必须处理它的各个元素。But once you retrieve the tuple, you have to handle its individual elements. 按元素逐个执行此操作会比较麻烦,如下例所示。Doing this on an element-by-element basis is cumbersome, as the following example shows. QueryCityData 方法返回一个 3 元组,并通过单独的操作将其每个元素分配给一个变量。The QueryCityData method returns a 3-tuple, and each of its elements is assigned to a variable in a separate operation.

using System;

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);
    }
}

从对象检索多个字段值和属性值可能同样麻烦:必须按成员逐个将字段值或属性值赋给一个变量。Retrieving multiple field and property values from an object can be equally cumbersome: you have to assign a field or property value to a variable on a member-by-member basis.

从 C# 7.0 开始,用户可从元组中检索多个元素,或在单个析构操作中从对象检索多个字段值、属性值和计算值。Starting with C# 7.0, you can retrieve multiple elements from a tuple or retrieve multiple field, property, and computed values from an object in a single deconstruct operation. 析构元组时,将其元素分配给各个变量。When you deconstruct a tuple, you assign its elements to individual variables. 析构对象时,将选定值分配给各个变量。When you deconstruct an object, you assign selected values to individual variables.

析构元组Deconstructing a tuple

C# 提供内置的元组析构支持,可在单个操作中解包一个元组中的所有项。C# features built-in support for deconstructing tuples, which lets you unpackage all the items in a tuple in a single operation. 用于析构元组的常规语法与用于定义元组的语法相似:将要向其分配元素的变量放在赋值语句左侧的括号中。The general syntax for deconstructing a tuple is similar to the syntax for defining one: you enclose the variables to which each element is to be assigned in parentheses in the left side of an assignment statement. 例如,以下语句将 4 元组的元素分配给 4 个单独的变量:For example, the following statement assigns the elements of a 4-tuple to four separate variables:

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

有三种方法可用于析构元组:There are three ways to deconstruct a tuple:

  • 可以在括号内显式声明每个字段的类型。You can explicitly declare the type of each field inside parentheses. 以下示例使用此方法来析构由 QueryCityData 方法返回的 3 元组。The following example uses this approach to deconstruct the 3-tuple returned by the QueryCityData method.

    public static void Main()
    {
        (string city, int population, double area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    
  • 可使用 var 关键字,以便 C# 推断每个变量的类型。You can use the var keyword so that C# infers the type of each variable. var 关键字放在括号外。You place the var keyword outside of the parentheses. 以下示例在析构由 QueryCityData 方法返回的 3 元组时使用类型推理。The following example uses type inference when deconstructing the 3-tuple returned by the QueryCityData method.

    public static void Main()
    {
        var (city, population, area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    

    还可在括号内将 var 关键字单独与任一或全部变量声明结合使用。You can also use the var keyword individually with any or all of the variable declarations inside the parentheses.

    public static void Main()
    {
        (string city, var population, var area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    

    这很麻烦,不建议这样做。This is cumbersome and is not recommended.

  • 最后,可将元组析构到已声明的变量中。Lastly, you may deconstruct the tuple into variables that have already been declared.

    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.
    }
    

请注意,即使元组中的每个字段都具有相同的类型,也不能在括号外指定特定类型。Note that you cannot specify a specific type outside the parentheses even if every field in the tuple has the same type. 这会生成编译器错误 CS8136:“析构 var (...) 形式不允许对 var 使用特定类型。”This generates compiler error CS8136, "Deconstruction 'var (...)' form disallows a specific type for 'var'.".

请注意,还必须将元组的每个元素分配给一个变量。Note that you must also assign each element of the tuple to a variable. 如果省略任何元素,编译器将生成错误 CS8132,“无法将 ‘x’ 元素的元组析构为 ‘y’ 变量”。If you omit any elements, the compiler generates error CS8132, "Cannot deconstruct a tuple of 'x' elements into 'y' variables."

请注意,不能混合析构左侧上现有变量的声明和赋值。Note that you cannot mix declarations and assignments to existing variables on the left-hand side of a deconstruction. 编译器生成错误 CS8184“析构不能混合左侧的声明和表达式”。The compiler generates error CS8184, "a deconstruction cannot mix declarations and expressions on the left-hand-side." 当成员包括新声明的和现有的变量。when the members include newly declared and existing variables.

使用弃元析构元组元素Deconstructing tuple elements with discards

析构元组时,通常只需要关注某些元素的值。Often when deconstructing a tuple, you're interested in the values of only some elements. 从 C# 7.0 开始,便可利用 C# 对弃元的支持,弃元是一种仅能写入的变量,且其值将被忽略。Starting with C# 7.0, you can take advantage of C#'s support for discards, which are write-only variables whose values you've chosen to ignore. 在赋值中,通过下划线字符 (_) 指定弃元。A discard is designated by an underscore character ("_") in an assignment. 可弃元任意数量的值,且均由单个弃元 _ 表示。You can discard as many values as you like; all are represented by the single discard, _.

以下示例演示了对元组使用弃元时的用法。The following example illustrates the use of tuples with discards. QueryCityDataForYears 方法返回一个 6 元组,包含城市名称、城市面积、一个年份、该年份的城市人口、另一个年份及该年份的城市人口。The QueryCityDataForYears method returns a 6-tuple with the name of a city, its area, a year, the city's population for that year, a second year, and the city's population for that second year. 该示例显示了两个年份之间人口的变化。The example shows the change in population between those two years. 对于元组提供的数据,我们不关注城市面积,并在一开始就知道城市名称和两个日期。Of the data available from the tuple, we're unconcerned with the city area, and we know the city name and the two dates at design-time. 因此,我们只关注存储在元组中的两个人口数量值,可将其余值作为占位符处理。As a result, we're only interested in the two population values stored in the tuple, and can handle its remaining values as discards.

using System;
using System.Collections.Generic;

public class Example
{
    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

析构用户定义类型Deconstructing user-defined types

对于非元组类型的解构,C# 不提供内置支持。C# does not offer built-in support for deconstructing non-tuple types. 但是,用户作为类、结构或接口的创建者,可通过实现一个或多个 Deconstruct方法来析构该类型的实例。However, as the author of a class, a struct, or an interface, you can allow instances of the type to be deconstructed by implementing one or more Deconstruct methods. 该方法返回 void,且要析构的每个值由方法签名中的 out 参数指示。The method returns void, and each value to be deconstructed is indicated by an out parameter in the method signature. 例如,下面的 Person 类的 Deconstruct 方法返回名字、中间名和姓氏:For example, the following Deconstruct method of a Person class returns the first, middle, and last name:

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

然后,可使用类似于以下的分配来析构名为 pPerson 类的实例:You can then deconstruct an instance of the Person class named p with an assignment like the following:

var (fName, mName, lName) = p;

以下示例重载 Deconstruct 方法以返回 Person 对象的各种属性组合。The following example overloads the Deconstruct method to return various combinations of properties of a Person object. 单个重载返回:Individual overloads return:

  • 名字和姓氏。A first and last name.
  • 名字、中间名和姓氏。A first, middle, and last name.
  • 名字、姓氏、城市名和省/市/自治区名。A first name, a last name, a city name, and a state name.
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 Example
{
    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 方法。Because you can overload the Deconstruct method to reflect groups of data that are commonly extracted from an object, you should be careful to define Deconstruct methods with signatures that are distinctive and unambiguous. 如果有多个 Deconstruct 方法具有相同数量的 out 参数,或具有相同数量和类型的 out 参数且顺序不同,则可能会造成混淆。Multiple Deconstruct methods that have the same number of out parameters or the same number and type of out parameters in a different order can cause confusion.

以下示例中的重载 Deconstruct 方法演示一种混淆的可能性。The overloaded Deconstruct method in the following example illustrates one possible source of confusion. 第一个重载按该顺序返回 Person 对象的名字、中间名、姓氏和年龄。The first overload returns the first name, middle name, last name, and age of a Person object, in that order. 第二个重载仅将姓名信息与年收入一起返回,但名字、中间名和姓氏的顺序不同。The second overload returns name information only along with annual income, but the first, middle, and last name are in a different order. 这使得在析构 Person 实例时容易混淆参数的顺序。This makes it easy to confuse the order of arguments when deconstructing a Person instance.

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 DateTime DateOfBirth { get; set; }
    public Decimal AnnualIncome { get; set; }

    public void Deconstruct(out string fname, out string mname, out string lname, out int age)
    {
        fname = FirstName;
        mname = MiddleName;
        lname = LastName;

        // calculate the person's age
        var today = DateTime.Today;
        age = today.Year - DateOfBirth.Year;
        if (DateOfBirth.Date > today.AddYears(-age))
            age--;
    }

    public void Deconstruct(out string lname, out string fname, out string mname, out decimal income)
    {
        fname = FirstName;
        mname = MiddleName;
        lname = LastName;
        income = AnnualIncome;
    }
}

使用弃元析构用户定义类型Deconstructing a user-defined type with discards

就像使用元组一样,可使用弃元来忽略 Deconstruct 方法返回的选定项。Just as you do with tuples, you can use discards to ignore selected items returned by a Deconstruct method. 每个弃元均由名为“_”的变量定义,一个析构操作可包含多个弃元。Each discard is defined by a variable named "_", and a single deconstruction operation can include multiple discards.

以下示例将 Person 对象析构为四个字符串(名字、姓氏、城市和省/市/自治区),但舍弃姓氏和省/市/自治区。The following example deconstructs a Person object into four strings (the first and last names, the city, and the state) but discards the last name and the state.

// Deconstruct the person object.
var (fName, _, city, _) = p;
Console.WriteLine($"Hello {fName} of {city}!");
// The example displays the following output:
//      Hello John of Boston!

使用扩展方法析构用户定义的类型Deconstructing a user-defined type with an extension method

如果没有创建类、结构或接口,仍可通过实现一个或多个 Deconstruct 扩展方法来析构该类型的对象,以返回所需值。If you didn't author a class, struct, or interface, you can still deconstruct objects of that type by implementing one or more Deconstruct extension methods to return the values in which you're interested.

以下示例为 System.Reflection.PropertyInfo 类定义了两个 Deconstruct 扩展方法。The following example defines two Deconstruct extension methods for the System.Reflection.PropertyInfo class. 第一个方法返回一组值,指示属性的特征,包括其类型、是静态还是实例、是否为只读,以及是否已编制索引。The first returns a set of values that indicate the characteristics of the property, including its type, whether it's static or instance, whether it's read-only, and whether it's indexed. 第二个方法指示属性的可访问性。The second indicates the property's accessibility. 因为 get 和 set 访问器的可访问性可能不同,所以布尔值指示属性是否具有单独的 get 和 set 访问器,如果是,则指示它们是否具有相同的可访问性。Because the accessibility of get and set accessors can differ, Boolean values indicate whether the property has separate get and set accessors and, if it does, whether they have the same accessibility. 如果只有一个访问器,或者 get 和 set 访问器具有相同的可访问性,则 access 变量指示整个属性的可访问性。If there is only one accessor or both the get and the set accessor have the same accessibility, the access variable indicates the accessibility of the property as a whole. 否则,get 和 set 访问器的可访问性由 getAccesssetAccess 变量指示。Otherwise, the accessibility of the get and set accessors are indicated by the getAccess and setAccess variables.

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 Example
{
    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

请参阅See also