在这篇文章中,我们将探讨如何使用集合表达式来简化代码,并了解初始化器、不同表达式的用法、支持的集合目标类型以及扩展语法。集合表达式是 C# 12 中引入的一个特性,它提供了一种在多种集合类型之间保持一致且简洁的语法。
初始化操作符 🌲
C# 提供了多种方式来初始化不同的集合,但集合表达式将所有这些整合在一起。让我们先来看看如何使用不同的方法初始化一个整数数组:
var numbers1 = new int[3] { 1, 2, 3 };
var numbers2 = new int[] { 1, 2, 3 };
var numbers3 = new[] { 1, 2, 3 };
int[] numbers4 = { 1, 2, 3 };
这四个版本功能上等效,编译器会为每个版本生成相同的代码。最后一个例子与新的集合表达式语法类似。如果你稍微眯眼,可以把大括号 {}
视作方括号 []
,那么看起来就像新的集合表达式语法。集合表达式不使用大括号,这是为了避免与现有语法(如模式匹配中的 {}
)产生混淆。
最后一个例子明确指定了类型,而不是使用 var
。例如,创建一个 List
:
List<string> david = ['D', 'a', 'v', 'i', 'd'];
请注意,集合表达式不能与 var
关键字一起使用,因为它们目前没有自然类型,可以转换为多种集合类型。支持对 var
的赋值还在考虑中。
当你尝试使用以下代码时,由于没有明确的目标类型,编译器会报错(CS9176):
// Error CS9176: There is no target type for the collection expression
var collection = [1, 2, 3];
你可能会问,既然有这么多初始化集合的方法,为什么还要使用新的集合表达式语法呢?答案在于,通过集合表达式,你可以使用统一的语法来表示各种集合,使代码更易读和维护。接下来我们会看到更多优点。
集合表达式的变体 🎭
- 你可以使用以下语法表示空集合:
int[] emptyCollection = [];
空集合表达式初始化是用 new
关键字替代旧代码的不错选择,因为它能被编译器优化,避免为某些集合类型分配内存。例如,对于数组 T[]
,编译器会生成 Array.Empty<T>()
,比 new int[] {}
更高效。
你还可以利用集合表达式中的元素数量设置集合大小,如
new List<int>(2)
对应于List<int> x = [1, 2];
。集合表达式允许你对接口赋值而不指定具体类型,编译器会根据类型(如
IEnumerable
、IReadOnlyList
和IReadOnlyCollection
)自动确定。如果实际类型很重要,你需要指定,因为随着更高效类型的出现,这可能会改变。对于不能生成更高效代码的情况(比如使用List
),编译器会生成new List<T>()
,然后效果相同。
使用空集合表达式的优点有三点:
- 统一初始化所有集合的方式,无论目标类型。
- 让编译器生成高效的代码。
- 缩短代码量,例如,代替
Array.Empty()
或Enumerable.Empty()
,只需写[]
。
关于生成的高效代码:使用 []
会产生已知的 IL,这样运行时可以优化重复使用 Array.Empty
的存储空间(针对每个 T
),甚至更激进地内联代码。
- 对于需要初始值的集合,你可以使用单个元素初始化,如下:
string[] singleElementCollection = ["one value in a collection"];
- 初始化多个元素的集合,只需添加更多常量值,例如:
int[] multipleElementCollection = [1, 2, 3 /* 可以有任意数量的元素 */];
历史回顾
这个特性的早期提案曾被称为“集合字面量”,与这个特性相关。这听起来很直观,特别是考虑到前面的例子。所有元素都是作为常量值表达的。但实际上,你可以用变量初始化集合,只要类型匹配(如果不匹配,会有隐式转换)。
来看一个使用“扩展元素”的例子,通过以下语法包含另一个集合的元素:
int[] oneTwoThree = [1, 2, 3];
int[] fourFiveSix = [4, 5, 6];
int[] all = [.. fourFiveSix, 100, .. oneTwoThree];
Console.WriteLine(string.Join(", ", all));
Console.WriteLine($"Length: {all.Length}"); // 输出: // 4, 5, 6, 100, 1, 2, 3 // Length: 7
扩展元素是一种强大的功能,允许你在当前集合中包含其他集合的元素。这是一种紧凑组合集合的好方法。扩展元素表达式内的表达式必须是可枚举的(即可 foreach 循环)。更多详细信息,请参阅扩展元素部分。
支持的集合类型 🎯
集合表达式可以用于许多目标类型,因为编译器识别代表集合的类型“形状”。因此,你熟悉的大多数集合默认支持。对于不符合“形状”的类型(主要是只读集合),你可以使用特定的属性描述构建器模式。BCL 中需要这些属性或构建器模式的集合类型已经更新过了。
通常情况下,你不需要关心目标类型的选择规则,但如果你对此感兴趣,可以查看 C# 语言参考:集合表达式——转换。
目前,集合表达式还不支持字典。你可以查阅提案了解如何扩展这个功能:C# 特性提案:字典表达式。
重构场景 🛠️
集合表达式在以下场景中很有用:
- 初始化声明非空集合类型的字段、属性、局部变量、方法参数、返回值或安全地避免异常的空合并表达式。
- 将参数传递给期望集合类型参数的方法。
现在我们来看一些使用场景和可能的重构机会。当你定义一个包含非空集合类型字段和/或属性的类或结构体时,可以用集合表达式进行初始化。例如,考虑 ResultRegistry
类:
namespace Collection.Expressions;
public sealed class ResultRegistry
{
private readonly HashSet<Guid> _results = new HashSet<Guid>();
public Guid RegisterResult(Result result)
{
_ = _results.Add(result);
return result.Id;
}
public void RemoveFromRegistry(Guid id)
{
_ = _results.RemoveWhere(x => x.Id == id);
}
}
public record class Result(bool IsSuccess, string? ErrorMessage)
{
public Guid Id { get; } = Guid.NewGuid();
}
在上面的代码中,ResultRegistry
类有一个私有 _results
字段,使用 new HashSet<Guid>()
构造表达式初始化。在支持这些重构功能的 IDE 中,右键点击 new
关键字,选择“快速操作和重构...”(或者按 +),然后选择“可以简化集合初始化”,如下所示:
代码会被更新为使用集合表达式语法:
private readonly HashSet<Guid> _results = [];
之前代码通过 new HashSet<Guid>()
构造表达式实例化,而在这里,[]
是完全等价的。
扩展元素 ✨
许多编程语言,如 Python 和 JavaScript/TypeScript 等,都有自己的“扩展”语法,用来简洁处理集合。在 C# 中,扩展元素(..
表达式)用于将多个集合合并到一个单一集合中。
正确术语
扩展元素常常与“扩展运算符”混淆。C# 中并没有“扩展运算符”,..
表达式不是一个运算符,它是扩展元素语法的一部分。本质上,它不是操作符,因为它并不对操作数执行操作。例如,..
在范围模式和列表模式中已经存在。
那么,什么是扩展元素?它从被“扩展”的集合中获取每个值,并将其放置在目标集合的相应位置。使用扩展元素还提供了重构的可能性。如果你的代码调用了 .ToList
或 .ToArray
,或者你想立即求值,IDE 可能会建议使用扩展元素语法。例如:
namespace Collection.Expressions;
public static class StringExtensions
{
public static List<Query> QueryStringToList(this string queryString)
{
List<Query> queryList = (from queryPart in queryString.Split('&')
let keyValue = queryPart.Split('=')
where keyValue.Length == 2
select new Query(keyValue[0], keyValue[1]))
.ToList();
return queryList;
}
}
public record class Query(string Name, string Value);
这段代码可以通过使用扩展元素重构,去除 .ToList
调用,甚至提供一个表达式体方法版本:
namespace Collection.Expressions
{
public static class StringExtensions
{
public static List<Query> QueryStringToList(this string queryString)
{
return (from queryPart in queryString.Split('&')
let keyValue = queryPart.Split('=')
where keyValue.Length == 2
select new Query(keyValue[0], keyValue[1]))
.ToList();
}
}
public record Query(string Name, string Value);
}
上述代码可以通过使用 展开元素 语法进行重构,去除 ToList()
方法调用,并采用表达式体方法作为额外的重构版本:
public static class StringExtensions
{
public static List<Query> QueryStringToList(this string queryString) =>
from queryPart in queryString.Split('&')
let keyValue = queryPart.Split('=')
where keyValue.Length == 2
select new Query(keyValue[0], keyValue[1]);
}
支持 Span
和 ReadOnlySpan
📏
集合表达式支持 Span
和 ReadOnlySpan
类型,它们表示任意内存连续区域。即使在代码中不直接使用,它们也能带来性能提升。当集合表达式用作参数时,运行时可以利用这些类型的优化。
如果应用中使用 span,可以直接赋值:
Span<int> numbers = [1, 2, 3, 4, 5];
ReadOnlySpan<char> name = ['D', 'a', 'v', 'i', 'd'];
如果使用 stackalloc
关键字,还提供了简化转换的重构选项。例如:
namespace Collection.Expressions
{
internal class Spans
{
public void Example()
{
ReadOnlySpan<byte> span = stackalloc byte[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
UseBuffer(span);
}
private static void UseBuffer(ReadOnlySpan<byte> span)
{
// TODO: 使用span...
throw new NotImplementedException();
}
}
}
右键点击 stackalloc
,选择 快速操作和重构...
(或按+),然后选择 集合初始化可以简化
,如视频所示:
代码会被更新为使用集合表达式语法:
namespace Collection.Expressions
{
internal class Spans
{
public void Example()
{
ReadOnlySpan<byte> span = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
UseBuffer(span);
}
// 为了简洁,省略了...
}
}
更多关于 Memory
和 Span
使用的指导,请参考相关文档。
语义考虑 ⚙️
使用集合表达式初始化集合时,编译器生成的功能上等同于使用集合初始化器的代码。有时生成的代码比使用初始化器更高效。看下面的例子:
List<int> someList = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
初始化器的规则要求编译器为初始化器中的每个元素调用 Add
方法。然而,如果使用集合表达式:
List<int> someList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
编译器会生成使用 AddRange
的代码,这可能更快或被优化得更好。因为编译器知道集合表达式的最终类型,所以能够做出这样的优化。
译自:https://devblogs.microsoft.com/dotnet/refactor-your-code-with-collection-expressions/
Comments