解构元组和其他类型

Tip

本文是已了解至少一种编程语言并正在学习 C# 的开发人员的 “基础知识 ”部分的一部分。 如果模式不熟悉,请从 模式匹配概述 开始。

解构将值的各个部分(其组件)分配给单个操作中的多个变量。 元组的分量是其元素,可按位置访问。 另一种类型可以通过定义 Deconstruct 方法来暴露组件。 位置记录类型(将其属性声明为类似构造函数的参数)会自动获得一个 Deconstruct 方法。

解构元组

假设某个方法返回包含城市数据的元组。 可以逐个读取每个组件:

var cityData = QueryCityData("New York City");
var city = cityData.City;
var population = cityData.Population;
var area = cityData.Area;

解构赋值可在一个步骤中将这些组成部分分别赋值:

(string city, int population, double area) = QueryCityData("New York City");

还可以让 C# 推断变量类型:

var (city, population, area) = QueryCityData("New York City");

解构可以在一个赋值中混合现有变量、新声明的变量和丢弃项:

(city, var population, _) = QueryCityData("New York City");

选择使代码更易于阅读的形式。 括号前的单个 var 值通常是最清晰的推断形式。 还可以在括号内混合显式类型和 var,但这种形式通常更难扫描。 如果只需要一些值,请使用丢弃符而不是省略位置。

使用舍弃项忽略不需要的值

每个生成的值都必须与赋值左侧的位置对齐。 如果不需要一个或多个位置,请使用 _ 作为丢弃符:

var (_, _, population1960, _, population2010) = QueryPopulationDataForYears(
    "New York City", 1960, 2010);

在这里,元组返回城市名称、两年和两个人口值。 解构仅保留总体数据值,因为计算仅使用这些组件。

分解用户定义类型

类、结构或接口可以通过声明 Deconstruct 方法来支持解构。 每个组成部分都会成为一个 out 参数,这样该方法就可以将值赋回调用方变量而不返回它。 由于每个组件都通过参数 out 返回,因此该方法本身返回 void:

public void Deconstruct(out string firstName, out string middleName, out string lastName)
{
    firstName = FirstName;
    middleName = MiddleName;
    lastName = LastName;
}

然后可以直接解构实例:

var (firstName, middleName, lastName) = passenger;

类型可以提供多个 重载,具有不同的 out(方法声明的 Deconstruct 参数数),因此调用方可以选择要检索的组件数:

public void Deconstruct(out string firstName, out string lastName)
{
    firstName = FirstName;
    lastName = LastName;
}

public void Deconstruct(out string firstName, out string middleName, out string lastName)
{
    firstName = FirstName;
    middleName = MiddleName;
    lastName = LastName;
}

public void Deconstruct(out string firstName, out string lastName, out string city, out string state)
{
    firstName = FirstName;
    lastName = LastName;
    city = City;
    state = State;
}

具有相同数量的 out 参数的两个重载存在歧义。 编译器会对歧义调用报告错误,因此,应通过元数区分重载,而不仅仅是通过参数类型。

丢弃也适用于用户定义的解构。 有关一般弃牌的详细信息,请参阅 “弃牌”和弃牌模式:

var (firstName, _, city, _) = passenger;

解构记录

位置record或record struct类型将其属性声明为类型声明本身的参数,类似于构造函数。 编译器会为你生成一个 Deconstruct 方法,其中包含 out 与这些位置参数匹配的参数:

var (city, highTempC, lowTempC) = forecast;

只有位置参数参与生成的析构。 不会自动添加记录上其他位置声明的其他属性。

解构你不拥有的类型

如果无法修改类型,仍可以通过编写扩展方法(一种静态方法)来支持解构,该方法会向你不拥有的类型添加 Deconstruct 方法,就好像它是该类型的成员一样。 添加该方法后,任何 Uri 值都可以使用解构语法:

static class UriExtensions
{
    public static void Deconstruct(this Uri uri, out string scheme, out string host, out int port)
    {
        scheme = uri.Scheme;
        host = uri.Host;
        port = uri.Port;
    }
}

此处同样适用相同的歧义规则:两个参数个数相同的扩展 Deconstruct 方法会产生歧义。 实例 Deconstruct 方法和参数个数相同的扩展方法之间也可能出现歧义。 在任一情况下,编译器都报告了不明确调用的错误。

系统类型的内置解构

某些系统类型已经定义了一个 Deconstruct 方法,使用与您自己的类型相同的机制。 例如,System.Collections.Generic.KeyValuePair<TKey,TValue> 支持解构,这使得字典迭代简洁:

foreach (var (repo, commitCount) in repoCommitCounts)
{
    Console.WriteLine($"{repo} had {commitCount:N0} commits in this snapshot.");
}

析构和模式匹配

Deconstruct 方法还启用该类型的 位置模式。 位置模式在一步中测试并解构一个值,使用与解构相同的括号语法:person is ("Alice", 30) 匹配其解构后的组件等于这些值的 Person。 这不同于 属性模式,该模式直接测试命名属性,例如 person is { Name: "Alice", Age: 30 }。 对于对象形状,属性模式通常更清晰,因为成员名称解释测试。 当顺序已包含含义时,位置模式最强,例如元组或其他较小的有序值。

另请参阅