02 七月 2025

C# 11 和 12 的最佳特性:少写代码,多做事情

在本文中,我们将介绍 C# 11 和 12 中引入的一些新特性,这些特性有助于简化代码并使开发过程更加顺畅。这些更新可能不是革命性的,但它们非常实用,旨在通过减少不必要的复杂性来节省时间。我们将看到这些小的变化如何在日常编码任务中带来更清晰、更高效的解决方案。

原始字符串字面量

在 C# 中,构建包含复杂内容的字符串历来是一个挑战。开发者经常需要处理特殊字符、换行符和引号的转义问题,导致代码冗长且往往难以阅读。在处理直接嵌入源文件中的 JSON、XML 或正则表达式等格式时,这个过程尤为繁琐。

C# 11 引入了原始字符串字面量,直接解决了这一痛点。此功能允许字符串跨多行,并且可以包含几乎任何字符,包括嵌入的引号和反斜杠,而无需使用转义序列。原始字符串字面量以至少三个双引号(""")开始和结束。

C# 11 之前:

string oldJson = "{\r\n  \"name\": \"Alice\",\r\n  \"age\": 30\r\n}";
Console.WriteLine(oldJson);

C# 11 之后:

string newJson = """
  {
    "name": "Alice",
    "age": 30
  }
  """;
Console.WriteLine(newJson);

结束引号前的任何空白字符定义了字符串的最小缩进,编译器会在最终输出中移除这些缩进。原始字符串字面量极大地提高了字符串的可读性,并减少了语法错误的可能性。

列表模式

C# 中的模式匹配有了显著的进化,C# 11 引入了列表模式,允许在数组或列表中进行序列匹配。这一增强功能使开发者能够简洁而富有表现力地检查集合的结构和内容。

以前,验证集合结构需要手动检查长度和各个索引,导致代码冗长且难以维护。列表模式通过支持常量、类型、属性和关系模式等子模式解决了这一问题。关键特性包括丢弃模式(_)用于匹配任何单个元素,以及范围模式(..)用于匹配零个或多个元素的任意序列。

C# 11 之前:

int[] numbers = { 1, 2, 3 };

if (numbers != null && numbers.Length == 3 &&
    numbers[0] == 1 && numbers[1] == 2 && numbers[2] == 3)
{
    Console.WriteLine("数组恰好包含 1, 2, 3。");
}

if (numbers != null && numbers.Length >= 2 && numbers[1] == 2)
{
    Console.WriteLine("数组的第二个元素是 2。");
}

C# 11 之后:

int[] numbers = { 1, 2, 3 };

if (numbers is [1, 2, 3])
{
    Console.WriteLine("数组恰好包含 1, 2, 3。");
}

if (numbers is [_, 2, ..])
{
    Console.WriteLine("数组的第二个元素是 2。");
}

列表模式将序列验证简化为紧凑、可读的形式,显著减少了此类操作所需的代码行数。

必需成员

对象初始化有时会导致不希望的状态,即关键属性或字段未被赋值。传统上,开发者通过接受所有必需参数的构造函数或在方法中添加防御性检查来强制进行必需的初始化。

C# 11 引入了属性和字段的 required 修饰符,这是一种编译时强制机制。当成员被标记为 required 时,编译器确保在对象创建期间为其赋值,无论是通过构造函数还是对象初始化器。这保证了类型的实例始终处于有效、完全初始化的状态,防止了与缺失数据相关的常见错误。

C# 11 之前:

public class OldPerson
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public void DisplayName() => Console.WriteLine($"姓名:{FirstName} {LastName}");
}

// 用法:
var person = new OldPerson(); // 没有编译时错误,但可能创建无效对象
person.DisplayName();

C# 11 之后:

public class NewPerson
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }

    public void DisplayName() => Console.WriteLine($"姓名:{FirstName} {LastName}");
}

// 用法:
// var person = new NewPerson(); // 编译错误 - 缺少必需属性
// var person = new NewPerson { FirstName = "John" }; // 编译错误 - 缺少 LastName
var person = new NewPerson { FirstName = "Jane", LastName = "Doe" }; // 正常
person.DisplayName();

必需成员通过在编译时强制初始化消除了运行时意外,减少了手动检查的需求。此功能通过减少防御性编码增强了代码的可靠性,使开发者能够专注于功能而非验证。

主构造函数

C# 12 为所有类和结构体引入了主构造函数,扩展了曾经仅限于记录类型的特性。这允许直接在类型定义中声明构造函数参数,并自动将其作用域扩展为整个类中的字段或属性。与传统构造函数不同,这种方法省略了显式的字段声明和手动赋值。

这里解决的关键问题是对象初始化中的重复样板代码。以前,开发者必须定义私有字段并显式地将构造函数参数映射到这些字段,无谓地增加了代码量。主构造函数简化了这一过程,将初始化逻辑直接嵌入到类型签名中。

C# 12 之前:

public class OldProduct
{
    private readonly int _productId;
    private readonly string _productName;

    public OldProduct(int productId, string productName)
    {
        _productId = productId;
        _productName = productName;
    }

    public string PrintDetails() => $"产品 ID:{_productId},名称:{_productName}";
}

// 用法:
OldProduct oldProd = new OldProduct(101, "笔记本电脑");
oldProd.PrintDetails();

C# 12 之后:

public class NewProduct(int productId, string productName)
{
    public string PrintDetails() => $"产品 ID:{productId},名称:{productName}";
}

// 用法:
NewProduct newProd = new NewProduct(102, "键盘");
newProd.PrintDetails();

主构造函数使数据中心类型的定义变得极其简洁。通过将基本构造参数直接放置在类型名称旁边,提高了可读性,使类或结构体的依赖关系一目了然。

集合表达式

在 C# 中初始化集合历来涉及不同的语法,具体取决于集合类型,例如列表使用 new List<T> { ... },数组使用 new T[] { ... }。将现有集合合并或组合成新集合通常需要迭代循环或使用 LINQ 方法(如 Concat()),增加了开销和冗长性。

C# 12 引入了集合表达式,这是一种统一且简洁的语法,用于创建和初始化各种集合类型。使用简单的 [...] 语法,开发者可以创建数组、列表、Span<T> 和其他类似集合的类型。新的扩展元素(..)允许直接将现有集合的元素内联到新的集合表达式中,消除了手动连接的需要。

C# 12 之前:

// 初始化不同的集合
int[] initialNumbers = new int[] { 1, 2, 3 };
List<int> moreNumbers = new List<int> { 4, 5 };

// 合并集合
List<int> allNumbers = new List<int>();
allNumbers.AddRange(initialNumbers);
allNumbers.AddRange(moreNumbers);
allNumbers.Add(6);
allNumbers.Add(7);

Console.WriteLine(string.Join(", ", allNumbers));

C# 12 之后:

// 初始化不同的集合
int[] initialNumbers = [1, 2, 3];
List<int> moreNumbers = [4, 5];

// 使用扩展运算符合并集合
List<int> allNumbers = [..initialNumbers, ..moreNumbers, 6, 7];

Console.WriteLine(string.Join(", ", allNumbers));

集合表达式减少了初始化和合并集合的冗长性,提供了更清晰、更直观的语法。这种高效性加速了编码并增强了可读性,支持用更少的代码实现更大影响的原则。

默认 Lambda 参数

Lambda 表达式是 C# 中函数式编程的基石,但历来无法为其参数定义默认值。如果 Lambda 需要处理可选参数或提供备用值,开发者不得不依赖 Lambda 体内的条件逻辑或定义多个重载,尽管 Lambda 并不直接支持重载。

C# 12 通过允许 Lambda 表达式中的参数具有默认值填补了这一空白。其语法和行为与方法或局部函数参数一致,提供了一种更流畅、更简洁的方式来定义灵活的 Lambda 函数。

C# 12 之前:

// 没有默认参数的 Lambda。
// 如果需要为 'y' 设置默认值,通常需要包装或条件逻辑:
Func<int, int, int> addOld = (x, y) => x + y;

Func<int, int> addWithDefaultOld = x => addOld(x, 10); // 常见的解决方法

Console.WriteLine(addOld(5, 3));
Console.WriteLine(addWithDefaultOld(5));

C# 12 之后:

// 带有默认参数的 Lambda
Func<int, int, int> addNew = (x, y = 10) => x + y;

Console.WriteLine(addNew(5, 3)); // y 为 3
Console.WriteLine(addNew(5));    // y 默认为 10

为 Lambda 引入默认参数显著增强了其灵活性和表现力。减少了重复 Lambda 定义或内部条件逻辑的需要。

结论

C# 11 和 12 带来了一组令人信服的特性,兑现了“少写代码,多做事情”的承诺。从 C# 11 的原始字符串字面量和列表模式到 C# 12 的主构造函数和集合表达式,这些进步解决了日常编码中的实际问题。它们去除了不必要的语法,提升了可读性,并强制执行更安全的模式,直接改善了软件开发和代码转换项目中的工作流程。每一项创新——无论是强制必需成员还是简化集合设置——都减少了按键次数,同时最大化清晰度并最小化错误风险。

相关文章