元组 为单个结构中的多个成员提供轻型数据结构。 它们相比匿名类型更受偏爱。 元组提供更好的性能、支持析构,并提供更灵活的语法。
匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。 编译器生成类型名称,在源代码层面无法访问。 编译器推断每个属性的类型。 主要当需要表达式树支持或处理需要引用类型的代码时,使用匿名类型。
元组与匿名类型
元组和匿名类型都允许你对多个值进行分组,而无需定义命名类型。 但是,元组具有更好的语言支持,并编译为更高效的数据结构。 下表对主要差异进行了汇总:
| 功能 / 特点 | 匿名类型 | 元组 |
|---|---|---|
| 类型 | 引用类型 (class) |
值类型 (struct) |
| Performance | 堆分配 | 堆栈分配(性能更佳) |
| Mutability | 只读属性 | 可变字段 |
| 解构 | 不支持 | 已支持 |
| 表达式树 | 已支持 | 不支持 |
| 访问修饰符 | internal |
public |
| 成员名称 | 必需或推断 | 可选(具有默认名称,例如 Item1, Item2) |
何时使用元组
在以下情况下使用元组:
- 需要通过堆栈分配获得更好的性能。
- 你想要将值解构为单独的变量。
- 你正在从方法中返回多个值。
- 不需要表达式树支持。
以下示例演示元组如何提供与具有更简洁语法的匿名类型类似的功能:
// Tuple with named elements.
var tupleProduct = (Name: "Widget", Price: 19.99M);
Console.WriteLine($"Tuple: {tupleProduct.Name} costs ${tupleProduct.Price}");
// Equivalent example using anonymous types.
var anonymousProduct = new { Name = "Widget", Price = 19.99M };
Console.WriteLine($"Anonymous: {anonymousProduct.Name} costs ${anonymousProduct.Price}");
元组解构
可以将元组解构为单独的变量,这为处理单个元组元素提供了一种便捷的方法。 C# 支持几种解构元组的方法:
static (string Name, int Age, string City) GetPersonInfo()
{
return ("Alice", 30, "Seattle");
}
// Deconstruct using var for all variables
var (name, age, city) = GetPersonInfo();
Console.WriteLine($"{name} is {age} years old and lives in {city}");
// Output: Alice is 30 years old and lives in Seattle
// Deconstruct with explicit types
(string personName, int personAge, string personCity) = GetPersonInfo();
Console.WriteLine($"{personName}, {personAge}, {personCity}");
// Deconstruct into existing variables
string existingName;
int existingAge;
string existingCity;
(existingName, existingAge, existingCity) = GetPersonInfo();
// Deconstruct and discard unwanted values using the discard pattern (_)
var (name2, _, city2) = GetPersonInfo();
Console.WriteLine($"{name2} lives in {city2}");
// Output: Alice lives in Seattle
解构在循环和模式匹配方案中非常有用:
var people = new List<(string Name, int Age)>
{
("Bob", 25),
("Carol", 35),
("Dave", 40)
};
foreach (var (personName2, personAge2) in people)
{
Console.WriteLine($"{personName2} is {personAge2} years old");
}
元组作为方法返回类型
元组的常见用例是作为方法返回类型。 可以使用元组来组合方法结果,而不是定义 out 参数。 无法从方法返回匿名类型,因为它没有名称,并且无法声明返回类型。
以下示例演示如何将元组与字典查找结合使用以返回配置范围:
var configLookup = new Dictionary<int, (int Min, int Max)>()
{
[2] = (4, 10),
[4] = (10, 20),
[6] = (0, 23)
};
if (configLookup.TryGetValue(4, out (int Min, int Max) range))
{
Console.WriteLine($"Found range: min is {range.Min}, max is {range.Max}");
}
// Output: Found range: min is 10, max is 20
使用需要返回成功指示器和多个结果值的方法时,此模式非常有用。 元组允许你使用命名字段(Min 和 Max),而不是像Item1和Item2这样的泛型名称,从而使代码更易于阅读和具有自解释性。
何时使用匿名类型
在以下情况下使用匿名类型:
- 你正在使用表达式树(例如,在某些Microsoft Language-Integrated 查询(LINQ)提供程序中)。
- 需要对象作为引用类型。
最常见的方案是用其他类型的属性初始化匿名类型。 在下面的示例中,假定名为 Product 的类存在。 类 Product 包括 Color 和 Price 属性,以及你不感兴趣的其他属性:
class Product
{
public string? Color { get; init; }
public decimal Price { get; init; }
public string? Name { get; init; }
public string? Category { get; init; }
public string? Size { get; init; }
}
匿名类型声明以 new 运算符开头,以及 对象初始值设定项。 声明初始化了一个只使用 Product 的两个属性的新类型。 匿名类型通常用于 select 查询表达式的子句中,以返回较少的数据。 有关查询的详细信息,请参阅C# 中的 LINQ。
如果未在匿名类型中指定成员名称,编译器会为匿名类型成员提供与用于初始化它们的属性相同的名称。 需要为使用表达式初始化的属性提供名称,如下面的示例所示。
在下面示例中,匿名类型的属性名称都为 ColorPrice 和 。 这些实例是类型集合中的productsProduct项:
var productQuery =
from prod in products
select new { prod.Color, prod.Price };
foreach (var v in productQuery)
{
Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price);
}
匿名类型投影初始化器
匿名类型支持 投影初始值设定项,这允许直接使用局部变量或参数,而无需显式指定成员名称。 编译器从变量名称推断成员名称。 以下示例演示了此简化的语法:
// Explicit member names.
var personExplicit = new { FirstName = "Kyle", LastName = "Mit" };
// Projection initializers (inferred member names).
var firstName = "Kyle";
var lastName = "Mit";
var personInferred = new { firstName, lastName };
// Both create equivalent anonymous types with the same property names.
Console.WriteLine($"Explicit: {personExplicit.FirstName} {personExplicit.LastName}");
Console.WriteLine($"Inferred: {personInferred.firstName} {personInferred.lastName}");
创建具有许多属性的匿名类型时,此简化的语法非常有用:
var title = "Software Engineer";
var department = "Engineering";
var salary = 75000;
// Using projection initializers.
var employee = new { title, department, salary };
// Equivalent to explicit syntax:
// var employee = new { title = title, department = department, salary = salary };
Console.WriteLine($"Title: {employee.title}, Department: {employee.department}, Salary: {employee.salary}");
以下情况下不会推断成员名称:
- 候选名与同一匿名类型中的另一个属性成员(显式或隐式)重复。
- 候选名称不是有效的标识符(例如,它包含空格或特殊字符)。
在这些情况下,必须显式指定成员名称。
提示
可以使用 .NET 样式规则 IDE0037 强制执行是首选推断成员名称还是显式成员名称。
还可以使用另一类型的对象(类、结构甚至其他匿名类型)来定义字段。 为此,请使用保存此对象的变量。 下面的示例演示了两种匿名类型,这些类型使用已实例化的用户定义类型。 在这两种情况下,product匿名类型的shipment字段属于类型shipmentWithBonus,Product并且包含每个字段的默认值。 该 bonus 字段是编译器创建的匿名类型。
var product = new Product();
var bonus = new { note = "You won!" };
var shipment = new { address = "Nowhere St.", product };
var shipmentWithBonus = new { address = "Somewhere St.", product, bonus };
通常,当使用匿名类型来初始化变量时,可以通过使用 var 将变量作为隐式键入的本地变量来进行声明。 不能在变量声明中指定类型名称,因为只有编译器有权访问匿名类型的基础名称。 有关 var 的详细信息,请参阅隐式类型本地变量。
可通过将隐式键入的本地变量与隐式键入的数组相结合创建匿名键入的元素的数组,如下面的示例所示。
var anonArray = new[] { new { name = "apple", diam = 4 }, new { name = "grape", diam = 1 }};
匿名类型是直接派生自object的class类型,您无法将它们强制转换为除object之外的任何类型。 编译器为每个匿名类型提供一个名称,尽管应用程序无法访问它。 从公共语言运行时的角度来看,匿名类型与任何其他引用类型没有什么不同。
如果程序集中的两个或多个匿名对象初始值指定了属性序列,这些属性采用相同顺序且具有相同的名称和类型,则编译器将对象视为相同类型的实例。 它们共享同一编译器生成的类型信息。
匿名类型支持采用 with 表达式形式的非破坏性修改。 使用此功能可以创建匿名类型的新实例,其中一个或多个属性具有新值:
var apple = new { Item = "apples", Price = 1.35 };
var onSale = apple with { Price = 0.79 };
Console.WriteLine(apple);
Console.WriteLine(onSale);
不能将字段、属性、事件或方法的返回类型声明为具有匿名类型。 同样,不能将方法、属性、构造函数或索引器的正式参数声明为具有匿名类型。 要将匿名类型或包含匿名类型的集合作为参数传递给某一方法,可将参数作为类型 object 进行声明。 但是,对匿名类型使用 object 违背了强类型的目的。 如果必须存储查询结果或者必须将查询结果传递到方法边界外部,请考虑使用普通的命名结构或类而不是匿名类型。
由于匿名类型上的 Equals 和 GetHashCode 方法是根据方法属性的 Equals 和 GetHashCode 定义的,因此仅当同一匿名类型的两个实例的所有属性都相等时,这两个实例才相等。
注意
匿名类型的 访问级别 为 internal。 因此,在不同程序集中定义的两个匿名类型不是同一类型。
因此,当在不同的程序集中定义时,匿名类型的实例不能彼此相等,即使其所有属性都相等。
匿名类型确实会重写 ToString 方法,将用大括号括起来的每个属性的名称和 ToString 输出连接起来。
var v = new { Title = "Hello", Age = 24 };
Console.WriteLine(v.ToString()); // "{ Title = Hello, Age = 24 }"