欢迎使用 C# 11

Amy Peng

本文翻译于Mads Torgersen的这篇英文文章:Welcome to C# 11

我很高兴地宣布 C# 11 已经发布!与往常一样,C# 开辟了一些全新的领域,同时推进了过去版本中一直在运行的几个主题。我们的文档页面上的 C# 11 的新增功能下有许多功能和详细信息,这些内容都得到了很好的介绍。 接下来是一些亮点开胃菜——有大有小。

在我们深入探讨之前,让我先说一下我对这个版本的 C# 的诞生有多高兴!随着每个版本的发布,社区的参与度越来越高,他们贡献了从建议、见解和bug报告一直到整个功能实现的所有内容。这真的是每个人的C#。谢谢!

UTF-8 字符串文字

默认情况下,C# 字符串被硬编码为 UTF-16,而互联网上流行的字符串编码是 UTF-8。 为了最大限度地减少转换的麻烦和性能开销,您现在只需将 u8 后缀附加到字符串文字即可立即将它们转换为 UTF-8:

var u8 = "This is a UTF-8 string!"u8;

UTF-8 字符串文字简单地返回一个字节块——以 ReadOnlySpan<byte> 的形式。 对于 UTF-8 编码很重要的场景,这可能比某些专用的新 UTF-8 字符串类型更有用。

阅读有关 UTF-8 字符串文字的文档。

原始字符串文字

字符串文字中的很多内容都是某种“代码”——不仅是程序文本,还有 JSON 和 XML 数据、HTML、正则表达式、SQL 查询等。 当有许多特殊字符在 C# 字符串文字中具有特殊含义时,问题就会显现! 值得注意的例子包括 \ 和 “,它们由 { 和 } 连接到内插字符串中。

为什么不采用一种完全没有转义字符的字符串文字形式呢? 这就是原始字符串文字。

原始字符串文字至少由三个双引号分隔:

var raw1 = """This\is\all "content"!""";Console.WriteLine(raw1);

输出:

This\is\all "content"!

如果您需要三个或更多的“作为内容的一部分,您只需在外部使用更多的”。 开头和结尾必须匹配:

var raw2 = """""I can do ", "", """ or even """" double quotes!""""";

这使得粘贴、维护和阅读文字包含的内容变得非常容易。

多行原始字符串文字也可以截断前导空格:结束引号的位置决定了输出中开始包含空格的位置:

var raw3 = """
    <element attr="content">
      <body>
        This line is indented by 4 spaces.
      </body>
    </element>
    """;
//  ^white space left of here is removed

由于右引号左侧有四个空格,因此从每行内容的开头删除四个空格,导致以下输出:

<element attr="content">
  <body>
    This line is indented by 4 spaces.
  </body>
</element>

原始字符串文字远不止于此——例如,它们支持字符串内插! 在文档中阅读有关原始字符串文字的更多信息。

对静态成员进行抽象

你如何抽象出本质上是静态的操作——比如运算符?传统的答案是“poorly”。在 C# 11 中,我们发布了对接口中静态虚拟成员的支持,这在 C# 10 中处于预览状态。有了它,您现在可以定义一个非常简单的数学接口:

public interface IMonoid<TSelf> where TSelf : IMonoid<TSelf>
{
    public static abstract TSelf operator +(TSelf a, TSelf b);
    public static abstract TSelf Zero { get; }
}

请注意接口如何为“self”获取类型参数。 那是因为静态成员没有this。

现在任何人都可以通过为两个静态成员提供实现并将它们自己作为TSelf类型参数传递来实现此接口:

public struct MyInt : IMonoid<MyInt>
{
    int value;
    public MyInt(int i) => value = i;
    public static MyInt operator +(MyInt a, MyInt b) => new MyInt(a.value + b.value);
    public static MyInt Zero => new MyInt(0);
}

重要的是,您如何使用这些抽象操作?当没有实例可以调用它们时,您如何调用虚拟成员?答案是通过泛型。这是它的样子:

T AddAll<T>(params T[] elements) where T : IMonoid<T>
{
    T result = T.Zero;
    foreach (var element in elements)
    {
        result += element;
    }
    return result;
}

类型参数 T 受 IMonoid<T> 接口约束,这允许在 T 本身上调用该接口的静态虚拟成员——Zero和 +!

现在我们可以用一些 MyInts 调用泛型方法,并通过类型参数传入 + 和 Zero 的正确实现:

MyInt sum = AddAll<MyInt>(new MyInt(3), new MyInt(4), new MyInt(5));

事实上,.NET 7 附带了一个新的命名空间 System.Numerics,其中充满了数学接口,表示您可能想要使用的运算符和其他静态成员的不同组合: IMonoid<T> 的“成熟”版本 接口如上。 .NET 中的所有数字类型现在都实现了这些新接口——您也可以将它们添加到您自己的类型中! 因此,现在很容易一劳永逸地编写数值算法——从它们处理的具体类型中抽象出来——而不是让大量的重载包含本质上相同的代码。

还值得注意的是,静态虚拟成员对于数学以外的其他事情也很有用。例如,您可以对类型层次结构的工厂方法进行抽象。但我们现在已经涵盖了足够多的内容——您可能想查看文档中关于静态抽象接口方法泛型数学的这些教程。

即使您不使用静态虚拟成员创建接口,您也可以从现在和将来对 .NET 库所做的改进中受益。

列表模式

模式匹配是 C# 中正在进行的故事之一,我们只是不断地填充它。模式匹配是在 C# 7 中引入的,从那时起它已经发展成为该语言中最重要和最强大的控制结构之一。

C# 11 将列表模式添加到故事中。 使用列表模式,您可以递归地将模式应用于类似列表的输入的单个元素——或者它们的范围。 让我们直接进入上面的通用算法,使用列表模式重写为递归方法:

T AddAll<T>(params T[] elements) where T : IMonoid<T> => 
    elements switch
{
    [] => T.Zero,
    [var first, ..var rest] => first + AddAll<T>(rest),
};

里面进行了很多事情,但在中心是一个带有两种情况的 switch 表达式。 一种情况为空列表 [] 返回零,其中Zero由接口定义。 另一种情况使用 var first 模式将第一个元素提取到 first 中,然后使用 .. 将剩余元素提取到 rest 中,以将所有剩余元素切出到 var rest 模式中。

在文档中阅读有关列表模式的更多信息。

必需的成员

我们一直致力于多个版本的另一个持续主题是改进对象创建和初始化。C# 11 继续对必需的成员进行这些改进。

在创建使用对象初始值设定项的类型时,您过去无法指定必须初始化某些属性。现在,您可以说属性或字段是必需的。 这意味着当创建该类型的对象时,它必须由对象初始值设定项初始化:

public class Person

{

    public required string FirstName { get; init; }

    public string? MiddleName { get; init; }

    public required string LastName { get; init; }

}

现在在没有初始化两个必需属性的情况下创建一个 Person 是错误的:

var person = new Person { FirstName = "Ada" }; // Error: no LastName!

查看文档以获取有关所需成员的更多信息。

最后

C# 11 还包括许多其他功能。 我希望这道开胃菜能激发您探索 C# 11 中的新功能,并希望您在使用 C# 11 进行编码时获得与我们编写它时一样多的乐趣!我们努力使语言变得更有用,不仅通过增加更多的表达能力(如静态虚拟成员),而且通过简化、精简和删除样板(如原始字符串文字和列表模式)并使其更安全。

感谢您使用C# 11.

0 comments

Leave a comment

Feedback usabilla icon