用集合表达式重构您的 C# 代码

Avatar
不若风吹尘
2024-06-19T18:08:13
285
0

在这篇文章中,我们将探讨如何使用集合表达式来简化代码,并了解初始化器、不同表达式的用法、支持的集合目标类型以及扩展语法。集合表达式是 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];

  • 集合表达式允许你对接口赋值而不指定具体类型,编译器会根据类型(如 IEnumerableIReadOnlyListIReadOnlyCollection)自动确定。如果实际类型很重要,你需要指定,因为随着更高效类型的出现,这可能会改变。对于不能生成更高效代码的情况(比如使用 List),编译器会生成 new List<T>(),然后效果相同。

使用空集合表达式的优点有三点:

  1. 统一初始化所有集合的方式,无论目标类型。
  2. 让编译器生成高效的代码。
  3. 缩短代码量,例如,代替 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]);
}

支持 SpanReadOnlySpan 📏

集合表达式支持 SpanReadOnlySpan 类型,它们表示任意内存连续区域。即使在代码中不直接使用,它们也能带来性能提升。当集合表达式用作参数时,运行时可以利用这些类型的优化。

如果应用中使用 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);
        }
        // 为了简洁,省略了...
    }
}

更多关于 MemorySpan 使用的指导,请参考相关文档。

语义考虑 ⚙️

使用集合表达式初始化集合时,编译器生成的功能上等同于使用集合初始化器的代码。有时生成的代码比使用初始化器更高效。看下面的例子:

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/

Last Modification : 9/20/2024 4:39:37 AM


In This Document