C# 15 随 .NET 11 发布,新增 union types 与 union 关键字
来源标题:C# 15: Union Types, Closed Hierarchies, and the Unique One Value
C# 15 随 .NET 11 在 .NET Conf 上发布,核心新特性是 union types,语言中新增 union 关键字,可直接声明如 Pet 为 Cat 或 Dog 的联合类型,并支持从各成员隐式转换。该版本为 net11.0 默认语言版本,无需预览标志即可使用。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
.NET 11 本周在 .NET Conf 上发布,C# 15 也随之而来。对我这样的人来说,最受关注的功能是 union 类型。多年来,人们一直希望在 C# 里使用可辨识联合(discriminated union),现在语言里终于有了 union 关键字。
开始之前先声明一下:C# 不是我的主场。我在前端写 Elm,也做过不少 Kotlin,还希望能多写一些 F#。所以这是一个局外人,在看一个自己很在意的功能,而用的并不是自己每天打交道的语言。如果我下面漏掉了某个众所周知的 C# 惯用法,我是真心想听听的。
我能做的是把代码跑起来。下面的每一段代码和每一条编译器消息,都来自 SDK 11.0.100-rc.3,也就是 GA 之前的最后一个候选版本。实际上不需要任何 preview 标志:C# 15 是 net11.0 的默认语言版本,单个 .cs 文件用 dotnet run file.cs 就能跑 🤓。
public record Cat(string Name); public record Dog(string Name, int Age); public union Pet(Cat, Dog);
就这样。一个 Pet 要么是 Cat,要么是 Dog,而且从两者各自都有隐式转换:
static string Describe(Pet pet) => pet switch Cat c => $"{c.Name} the cat", Dog d => $"{d.Name} the dog, age {d.Age}", }; Pet pet = new Dog("Fido", 3); Describe(pet); Fido the dog, age 3
注意这里没有 _ => 分支。在 C# 15 之前,对接口或抽象基类写这样的 switch,除非你在末尾加一个 discard,否则会收到警告,因为编译器无从知道不会有人在某处加一个 Hamster。而用 union,它知道完整的列表(换句话说,它是穷尽的),所以能告诉你漏了哪一个。
我们来加一只鸽子(总会有只鸽子,对吧?):
public record Pigeon(string Name); public union Pet(Cat, Dog, Pigeon);
Describe 保持原样:
warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'Pigeon' is not covered.
只是一条警告。就差那么一点点,我是说,“让不可能的状态变得不可能”明明就在眼前!程序仍然能构建和运行,而如果真有 Pigeon 到达 Describe,它会在运行时抛异常。
公平地说,F# 做的完全一样:
type Pet = | Cat of name: string | Dog of name: string * age: int | Pigeon of name: string let describe pet = match pet with | Cat name -> $"{name} the cat" | Dog (name, age) -> $"{name} the dog, age {age}" warning FS0025: Incomplete pattern matches on this expression. For example, the value 'Pigeon (_)' may indicate a case not covered by the pattern(s).
三者之中,只有 Elm 会直接拒绝:
-- MISSING PATTERNS ----------------------------------------------- src/Main.elm
This case does not have branches for all possibilities:
...
Missing possibilities include:
Pigeon _
I would have to crash if I saw one of those. Add branches for them!
“I would have to crash if I saw one of those.” 这句话真是让人喜欢。而这恰恰就是重点:编译器知道程序可能崩溃,而在 C# 和 F# 里,它礼貌地告诉你,然后还是让你发布出去。抱歉,我跑题了。
所以,在任何 C# 15 代码库里,我要做的第一件事就是把那条警告变成错误。在 .csproj 里是 <WarningsAsErrors>CS8509</WarningsAsErrors>;在基于文件的 app 里,就是顶部的一行:
#:property WarningsAsErrors=CS8509 error CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'Pigeon' is not covered. The build failed. Fix the build errors and run again.
好多了。(顺便说一句,在 F# 里我也会对 FS0025 做同样的事。)
现在要小心:穷尽性检查只适用于 switch 表达式。老牌的 switch 语句可享受不到:
Pet pet = new Pigeon("Kevin"); switch (pet) case Cat c: Console.WriteLine($"feed {c.Name} some fish"); break; case Dog d: Console.WriteLine($"walk {d.Name}"); break; Console.WriteLine("done"); done
我想有人会说,这和 switch 语句在 C# 里一贯的行为是一致的(也就是说,它们允许什么都不做),但这意味着安全网只在你以某种方式写代码时才存在。如果我要在现有代码库里采用 union,我会想要一个分析器,把每个 switch (someUnion) 都推向 switch 表达式。我找到的最接近的内置规则 IDE0010(“Populate switch”)在这里帮不上忙:它对每个 switch 语句都会触发,哪怕这个语句已经覆盖了 union 的所有 case,因为它想要一个 default: 分支(而那会破坏本来的目的)。说实话,这方面我不太满意。
这是与 F# 和 Elm 最大的概念差异:在 F# 里,DU 的 case 是标签:Email of string 和 Phone of string 是两个不同的 case,尽管它们都携带一个 string。而 C# 的 union 是类型的联合。规范对此直言不讳,编译器也是:
public union ContactInfo(string, string); error CS0111: Type 'ContactInfo' already defines a member called 'ContactInfo' with the same parameter types
你也不能内联声明 case,所以每个 case 都必须是某处已存在的类型。实践中这意味着每个 case 一个 record,比 F# 的 | Email of string 要多一点仪式感。我承认,这会把你推向一个我本来就会推荐的做法:把基元类型包装进恰当的领域类型(用 EmailAddress record 而不是裸 string)。我想 Scott Wlaschin 会赞同的。
同样的问题还会以更隐蔽、更微妙的方式影响泛型。这是你的普通 Result:
public union Result<T, E>(T, E); Result<int, string> ok = 42; // fine Result<string, string> uhOh = "is this an error?"; error CS0457: Ambiguous user defined conversions 'Result<string, string>.Result(string)' and 'Result<string, string>.Result(string)' when converting from 'string' to 'Result<string, string>'
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。