原始字符串字面量

原始字符串字面量是C# 11中一个简化字符串处理的重要特性。它允许开发人员创建包含任意字符(如换行符、引号、反斜杠等)的字符串,而无需使用转义序列。其语法是使用至少三个双引号来界定字符串的开始和结束。

基本用法与优势

在C# 11之前,编写包含大量特殊字符(例如JSON、XML、SQL语句或正则表达式)的字符串会非常繁琐,因为每个引号和反斜杠都需要转义。原始字符串字面量彻底解决了这个问题。字符串内容从第一个换行符之后开始,在结束引号之前的最后一个换行符处结束,内部的缩进格式也会被保留。这使得代码中的嵌入式文本更加清晰易读。

示例

下面的代码展示了如何使用原始字符串字面量来定义一个JSON字符串:

string jsonString =     {        name: John Doe,        age: 30,        isActive: true,        hobbies: [reading, hiking]    }    ;

在这个例子中,所有双引号和换行符都无需转义,代码的可读性得到了极大提升。

必需的成员

必需的成员特性通过在属性或字段上使用required修饰符,强制要求对象初始化器必须在创建对象时为这些成员赋值。这为构建不可变数据类型和确保对象在初始化时就处于有效状态提供了强有力的支持。

解决的问题

在过去,确保某个属性在对象构造后被赋值通常依赖于构造函数参数,或者依赖于开发人员的自觉性。如果忘记初始化,可能会导致运行时错误。使用required修饰符,编译器会在使用对象初始化器时强制检查,如果遗漏了任何必需的成员,将产生编译错误。

如何使用

首先,在属性声明前加上required关键字。然后,在创建对象时,必须使用对象初始化器为所有必需的属性赋值。

public class Person{    public required string FirstName { get; set; }    public required string LastName { get; set; }    public int Age { get; set; } // 非必需属性}// 正确用法:初始化所有必需成员Person person = new Person { FirstName = Jane, LastName = Doe };// 编译错误:未初始化必需的属性 LastName// Person invalidPerson = new Person { FirstName = John };

此特性与C# 9引入的init访问器结合使用效果更佳,可以创建在初始化后不可变的属性。

列表模式匹配

C# 11极大地扩展了模式匹配的能力,特别是引入了列表模式,允许对数组或列表等序列进行更直观、更强大的匹配操作。你可以匹配序列的特定长度、检查开头或结尾的元素,甚至使用切片模式匹配子序列。

强大的匹配模式

列表模式使用方括号[]来定义匹配模式。在模式中,可以使用单个元素、范围操作符..(表示零个或多个元素的切片)或弃元_(表示匹配单个任意元素)。

实际应用示例

假设我们有一个表示命令行参数的字符串数组,我们可以使用列表模式来解析它:

string[] args = new[] { compile, --output, myapp.exe, program.cs };var message = args switch{    [compile, --output, var outputFile, var inputFile] => $编译 {inputFile} 到 {outputFile},    [run, var programName, ..] => $运行程序 {programName},    [--help] => 显示帮助信息,    [] => 未提供任何参数,    _ => 无法识别的命令};Console.WriteLine(message); // 输出:编译 program.cs 到 myapp.exe

这个特性在处理集合数据时极为灵活,能够替代大量复杂的if-else逻辑,使代码更加简洁和声明式。

泛型数学支持

泛型数学是C# 11和.NET 7中一项颠覆性的特性,它允许开发人员编写能够对多种数值类型进行算术运算的泛型代码。这主要通过引入静态抽象接口成员来实现。

打破泛型约束的局限

在此之前,C#的泛型约束(如where T : INumber)不支持指定算术运算符。泛型数学特性定义了一系列与数值运算相关的接口(如INumber<TSelf>IAdditionOperators<TSelf, TOther, TResult>),这些接口包含了静态抽象成员,从而使得在泛型类型上调用+-/等运算符成为可能。

实战示例

现在我们可以编写一个通用的“计算平均值”的方法,它可以同时处理int, double, decimal等类型:

public static T Average<T>(IEnumerable<T> values) where T : INumber<T>{    T sum = T.Zero;    T count = T.Zero;    foreach (T value in values)    {        sum += value;        count++;    }    return sum / count;}// 使用示例List<int> intNumbers = new() { 1, 2, 3, 4, 5 };List<double> doubleNumbers = new() { 1.5, 2.5, 3.5 };Console.WriteLine(Average(intNumbers));   // 输出: 3Console.WriteLine(Average(doubleNumbers)); // 输出: 2.5

这个特性为开发数学库、金融计算和游戏引擎等需要高性能通用计算的场景打开了新的大门。

文件本地类型

文件本地类型通过使用file访问修饰符,可以将类型(如classrecordstruct)的可见性限制在声明它的源文件内部。这是C#首次引入文件级别的访问控制,旨在更好地封装代码,防止在大型项目中意外地从一个文件访问另一个文件的内部类型。

提升代码封装性

当一个类型只被单个文件中的代码使用时,将其声明为file类型是最佳选择。它可以避免该类型污染全局命名空间,并确保其他开发人员不会错误地依赖这个本应私有的实现细节。

代码示例

假设我们有一个文件DataProcessor.cs,其中包含一个仅为该文件内部使用的辅助类:

// DataProcessor.csfile record ProcessingResult(bool Success, string Message);public class DataProcessor{    public void Process()    {        var result = DoInternalWork();        // ... 使用 result    }    private ProcessingResult DoInternalWork()    {        return new ProcessingResult(true, 处理完成);    }}// 在另一个文件中尝试访问 ProcessingResult 会导致编译错误// FileLocalTypeDemo.ProcessingResult result; // 错误:无法访问文件本地类型

这个特性特别适合在源代码生成器或拥有大量文件的复杂项目中维护清晰的代码边界。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐