技术速递|使用 C# 集合表达式重构代码

作者:David Pine
排版:Alan Wang

本文是系列文章的第二篇,该系列文章涵盖了探索 C# 12功能的各种重构场景。在这篇文章中,我们将了解如何使用集合表达式重构代码,我们将学习集合初始化器、各种表达式用法、支持的集合目标类型和 spread 语法。该系列的进展情况如下:

  1. 使用主构造函数重构 C# 代码
  2. 使用集合表达式重构 C# 代码(本文)
  3. 通过为任何类型添加别名来重构您的 C# 代码
  4. 重构您的 C# 代码以使用默认 lambda 参数

这些功能延续了我们的旅程,使我们的代码更具可读性和可维护性,并且被认为是开发人员应该了解的“日常 C#”功能。

集合表达式

C# 12 引入了集合表达式,它为许多不同的集合类型提供简单且一致的语法。当使用集合表达式初始化集合时,编译器生成的代码在功能上与使用集合初始化项等效。该功能强调一致性,同时允许编译器优化低级的 C#。当然,每个团队都可以决定采用哪些新功能,如果您愿意,您可以尝试并引入这种新语法,因为之前所有初始化集合的方法都将继续工作。

对于集合表达式,元素出现在左括号 [ 和右括号 ] 之间的内联元素序列。继续阅读以了解有关集合表达式如何工作的更多信息。

初始化

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<char> david = [ 'D', 'a', 'v', 'i', 'd' ];

同样,集合表达式不能与 var 关键字一起使用。您必须声明类型,因为集合表达式目前没有自然类型,以及可以转换为多种集合类型。对 var 赋值的支持仍在考虑中,但团队尚未确定自然类型应该是什么。换句话说,在编写以下代码时,C# 编译器会出错并显示 CS9176:集合表达式没有目标类型:

// Error CS9176: There is no target type for the collection expression
var collection = [1, 2, 3];

您可能会问自己,“既然有这么多不同的方法来初始化集合,为什么我要使用新的集合表达式语法?” 答案是,通过集合表达式,您可以使用相同的语法以一致的方式表达集合。这有助于提高代码的可读性和可维护性。我们将在接下来的部分中探讨更多优势。

集合表达式变化

您可以使用以下语法表示集合为空:

int[] emptyCollection = [];

​空集合表达式的初始化是代替以前使用“new”关键字的代码的绝佳选择,因为它已被编译器优化,以避免为某些集合类型分配内存。例如,当集合类型是数组 T[] 时,编译器会生成 Array.Empty(),它比 new int[] { } 效率更高。另一种快捷方式是使用集合表达式中的元素数量来设置集合大小,例如对于 Listx = [1, 2];使用 new List(2)。

集合表达式还允许您在不声明显式类型的情况下赋值给接口。编译器确定用于 IEnumerable、IReadOnlyList和 IReadOnlyCollection等类型的类型。如果实际使用的类型很重要,您需要声明它,因为如果有更高效的类型可用,情况可能会发生变化。同样,在编译器无法生成更高效的代码的情况下,例如当集合类型是 List时,编译器会生成一个新的 List(),它是等效的。

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

  • 它提供了初始化所有集合的一致方法,无论其目标类型如何。
  • 它允许编译器生成高效的代码。
  • 需要编写的代码更少。例如,您可以简单地编写 [],而不是编写 Array.Empty()或 Enumerable.Empty()。

关于高效生成代码的更多细节:使用 [] 语法生成已知的 IL。这允许运行时通过重用 Array.Empty(对于每个 T)的存储来优化,甚至更积极地内联代码。

空集合可以满足它们的目的,但是您可能需要一个具有一些初始值的集合。您可以使用以下语法用单个元素初始化集合:

string[] singleElementCollection =
[
    "one value in a collection"
];

初始化单个元素集合类似于初始化包含多个单个元素的集合。您可以使用以下语法通过添加其他文字值来初始化包含多个元素的集合:

int[] multipleElementCollection = [1, 2, 3 /* any number of elements */];

一些历史
该功能的早期提案包括短语“集合文字”,您可能听说过与此功能相关的术语。这似乎是显而易见且合乎逻辑的,特别是考虑到前面的几个例子。 所有元素均表示为文字值。 但您不局限于使用文字。事实上,只要类型一致,您就可以轻松地使用变量初始化集合(当它们不对应时,可以使用隐式转换)。

让我们看另一个代码示例,但它使用 spread 元素来包含另一个集合的元素,使用以下语法:

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}");
// Outputs:
//   4, 5, 6, 100, 1, 2, 3
//   Length: 7

Spread 元素是一个强大的功能,它允许您将另一个集合的元素包含在当前集合中。spread 元素是一种以简洁的方式组合集合的好方法。Spread 元素中的表达式必须是可枚举的(可查询的)。有关更多信息,请参阅 Spread 部分。

支持的集合类型

集合表达式可以与许多目标类型一起使用。该功能可识别代表集合类型的“形状”。因此,您熟悉的大多数集合都是开箱即用的。对于与该“形状”不匹配的类型(主要是只读集合),您可以应用一些属性来描述构建器模式。BCL 中需要属性/构建器模式方法的集合类型已经更新。

重构场景

集合表达式在许多场景中都很有用,例如:

  • 初始化声明非空集合类型的空集合:
    • 字段
    • 属性
    • 局部变量
    • 方法参数
    • 返回值
    • 合并表达式作为最终的解决方案,以安全地避免异常
  • 将参数传递给需要集合类型参数的方法

让我们利用本节来探索一些示例使用场景,并考虑潜在的重构机会。当您定义包含非空集合类型的字段和/或属性的类或结构时,可以使用集合表达式来初始化它们。例如,请考虑以下 ResultRegistry 对象示例:

namespace Collection.Expressions;
public sealed class ResultRegistry
{
    private readonly HashSet<Result> _results = new HashSet<Result>();
    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();
}

在前面的代码中,结果注册表类包含一个私有 _results 字段,该字段使用新的 HashSet()构造函数表达式进行初始化。在您选择的 IDE(支持这些重构功能)中,右键单击 new 关键字,选择 Quick Actions and Refactorings…(或按Ctrl + .),然后选择“Collection initialization can be simplified”,如下视频所示:

refactor-simplify-collection

代码已更新为使用集合表达式语法,如以下代码所示:

private readonly HashSet<Result> _results = [];

前面的代码使用 new HashSet()构造函数表达式实例化了 HashSet。 然而,在这种情况下 [] 是等效的。

Spread

许多流行的编程语言(例如 Python 和 JavaScript/TypeScript 等)都提供了 spread 语法的变体,这是一种简洁的处理集合的方式。在 C# 中,spread 元素是用于将各种集合串联成单个集合的语法。

正确的术语
Spread 元素经常与术语“spread运算符”混淆。在 C# 中,不存在“spread运算符”这样的东西。… 表达式不是运算符,它是 spread 元素语法一部分的表达式。根据定义,此语法与运算符的语法不一致,因为它不对操作数执行操作。例如,… 表达式已经存在于范围切片模式中,并且也可以在列表模式中找到。

那么 spread 元素到底是什么?它从正在“spread”的集合中获取各个值,并将它们放置在目标集合中的相应位置。Spread 元素功能还带来了重构机会。如果您有调用 .ToList 或 .ToArray 的代码,或者您想要使用即时求值,您的 IDE 可能会建议改用 spread 元素语法。例如,以下代码:

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 is 2
            select new Query(keyValue[0], keyValue[1])
        )
        .ToList()
;
        return queryList;
    }
}
public record class Query(string Name, string Value);

可以重构前面的代码以使用 spread 元素语法,请考虑以下代码,该代码删除了 .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 is 2
           select new Query(keyValue[0
], keyValue[
1
])
    ];
}

Span 和 ReadOnlySpan 支持

集合表达式支持 Span和 ReadOnlySpan类型,用于表示任意内存的连续区域。即使您不在代码中直接使用它们,您也可以从它们提供的性能改进中受益。集合表达式允许运行时提供优化,特别是当集合表达式用作参数时可以选择使用 span 的重载。

如果您的应用程序使用 span,您也可以直接赋值给 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:
        //   Use the span...
        throw new NotImplementedException();
    }
}

如果右键 stackalloc 关键字,选择 Quick Actions and Refactorings…(或者按 Ctrl + .),选择 Collection initialization can be simplified,如下视频所示:

refactor-collection-ex

代码已更新为使用集合表达式语法,如以下代码所示:

namespace Collection.Expressions;
internal class Spans
{
    public void Example()
    {
        ReadOnlySpan<byte> span =
        [
            1, 2, 3, 4, 5, 6, 7, 8, 9, 10
];
        UseBuffer(span);
    }
    // Omitted for brevity...
}

有关详细信息,请参阅 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,这可能更快或更优化。 编译器能够进行这些优化,因为它知道集合表达式的目标类型。

后续步骤

请务必在您自己的代码中尝试一下!敬请期待本系列的下一篇文章,我们将探讨如何通过为任何类型添加别名来重构 C# 代码。同时,您可以在以下资源中了解有关集合表达式的更多信息:

  • 29
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值