C# 10 完整特性介绍 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
hez2010
V2EX    C#

C# 10 完整特性介绍

  •  6
     
  •   hez2010 2021-08-09 16:27:23 +08:00 4109 次点击
    这是一个创建于 171 天前的主题,其中的信息可能已经有所发展或是发生改变。

    一直忘了 V2EX 有个 C# 节点,想着分享点东西增加一些讨论热度,就介绍一下 C# 10 最终敲定的特性吧。总的来说 C# 10 的更新内容很多,并且对类型系统做了不小的改动,解决了非常多现有的痛点。

    从 C# 10 可以看到一个消息,那就是 C# 语言团队开始主要着重于改进类型系统和功能性方面的东西,而不是像以前那样热衷于各种语法糖了。C# 10 只是这个旅程的开头,后面的 C# 11 、12 将会有更多关于类型系统的改进,使其拥有强如 Haskell 、Rust 的表达能力,不仅能提供从头到尾的跨程序集的静态类型支持,还能做到像动态类型语言那样的灵活,而不是诸如什么 objectdynamicvoid**interface{} 之类的东西。逻辑代码是类型的证明,只有类型系统强大了,代码编写起来才能更顺畅、更不容易出错。

    record struct

    首先自然是 record struct,解决了 record 只能给 class 而不能给 struct 用的问题:

    record struct Point(int X, int Y); 

    用 record 定义 struct 的好处其实有很多,例如你无需重写 GetHashCodeEquals 之类的方法了。

    sealed record ToString 方法

    之前 record 的 ToString 是不能修饰为 sealed 的,因此如果你继承了一个 record,相应的 ToString 行为也会被改变,因此这是个虚方法。

    但是现在你可以把 record 里的 ToString 方法标记成 sealed,这样你的 ToString 方法就不会被重写了。

    struct 无参构造函数

    一直以来 struct 不支持无参构造函数,现在支持了:

    struct Foo { public int X; public Foo() { X = 1; } } 

    但是使用的时候就要注意了,因为无参构造函数的存在使得 new struct()default(struct) 的语义不一样了,例如 new Foo().X == default(Foo).X 在上面这个例子中将会得出 false

    匿名对象的 with

    可以用 with 来根据已有的匿名对象创建新的匿名对象了:

    var x = new { A = 1, B = 2 }; var y = x with { A = 3 }; 

    这里 y.A 将会是 3 。

    全局的 using

    利用全局 using 可以给整个项目启用 usings,不再需要每个文件都写一份。比如你可以创建一个 Import.cs,然后里面写:

    using System; using i32 = System.Int32; 

    然后你整个项目都无需再 using System,并且可以用 i32 了。

    文件范围的 namespace

    这个比较简单,以前写 namespace 还得带一层大括号,以后如果一个文件里只有一个 namespace 的话,那直接在最上面这样写就行了:

    namespace MyNamespace; 

    常量字符串插值

    你可以给 const string 使用字符串插值了,非常方便:

    const string x = "hello"; const string y = $"{x}, world!"; 

    lambda 改进

    这个改进可以说是非常大,我分多点介绍。

    1. 支持 attributes

    lambda 可以带 attribute 了:

    f = [Foo] (x) => x; // 给 lambda 设置 f = [return: Foo] (x) => x; // 给 lambda 返回值设置 f = ([Foo] x) => x; // 给 lambda 参数设置 

    2. 支持指定返回值类型

    此前 C# 的 lambda 返回值类型靠推导,C# 10 开始允许在参数列表最前面显示指定 lambda 类型了:

    f = int () => 4; 

    3. 支持 ref 、in 、out 等修饰

    f = ref int (ref int x) => ref x; // 返回一个参数的引用 

    4. 头等函数

    函数可以隐式转换到 delegate,于是函数上升至头等函数:

    void Foo() { Console.WriteLine("hello"); } var x = Foo; x(); // hello 

    5. 自然委托类型

    lambda 现在会自动创建自然委托类型,于是不再需要写出类型了。

    var f = () => 1; // Func<int> var g = string (int x, string y) => $"{y}{x}"; // Func<int, string, string> var h = "test".GetHashCode; // Func<int> 

    CallerArgumentExpression

    现在,CallerArgumentExpression 这个 attribute 终于有用了。借助这个 attribute,编译器会自动填充调用参数的表达式字符串,例如:

    void Foo(int value, [CallerArgumentExpression("value")] string? expression = null) { Console.WriteLine(expression + " = " + value); } 

    当你调用 Foo(4 + 5) 时,会输出 4 + 5 = 9。这对测试框架极其有用,因为你可以输出 assert 的原表达式了:

    static void Assert(bool value, [CallerArgumentExpression("value")] string? expr = null) { if (!value) throw new AssertFailureException(expr); } 

    tuple 支持混合定义和使用

    比如:

    int y = 0; (var x, y, var z) = (1, 2, 3); 

    于是 y 就变成 2 了,同时还创建了两个变量 x 和 z,分别是 1 和 3 。

    接口支持抽象静态方法

    这个特性将会在 .NET 6 作为 preview 特性放出,意味着默认是不启用的,需要设置 <LangVersion>preview</LangVersion><EnablePreviewFeatures>true</EnablePreviewFeatures>,然后引入一个官方的 nuget 包 System.Runtime.Experimental 来启用。

    然后接口就可以声明抽象静态成员了,.NET 的类型系统正式具备虚静态方法分发能力。

    例如,你想定义一个可加而且有零的接口 IMonoid

    interface IMonoid<T> where T : IMonoid<T> { abstract static T Zero { get; } abstract static T operator+(T l, T r); } 

    然后可以对其进行实现,例如这里的 MyInt

    public class MyInt : IMonoid<MyInt> { public MyInt(int val) { Value = val; } public static MyInt Zero { get; } = new MyInt(0); public static MyInt operator+(MyInt l, MyInt r) => new MyInt(l.Value + r.Value); public int Value { get; } } 

    然后就能写出一个方法对 IMoniod<T> 进行求和了,这里为了方便写成扩展方法:

    public static class IMonoidExtensions { public static T Sum<T>(this IEnumerable<T> t) where T : IMonoid<T> { var result = T.Zero; foreach (var i in t) result += i; return result; } } 

    最后调用:

    List<MyInt> list = new() { new(1), new(2), new(3) }; Console.WriteLine(list.Sum().Value); // 6 

    你可能会问为什么要引入一个 System.Runtime.Experimental,因为这个包里面包含了 .NET 基础类型的改进:给所有的基础类型都实现了相应的接口,比如给数值类型都实现了 INumber,给可以加的东西都实现了 IAdditionOperator<TLeft, TRight, TResult> 等等,用起来将会非常方便,比如你想写一个函数,这个函数用来把能相加的东西加起来:

    T Add<T>(T left, T right) where T : IAdditionOperator<T, T, T> { return left + right; } 

    就搞定了。

    接口的静态抽象方法支持和未来 C# 将会加入的 shape 特性是相辅相成的,届时 C# 将利用 interfaceshape 支持 Haskell 的 class、Rust 的 trait 那样的 type classes,将类型系统上升到一个新的层次。

    泛型 attribute

    是的你没有看错,C# 的 attributes 支持泛型了:

    class TestAttribute<T> : Attribute { public T Data { get; } public TestAttribute(T data) { Data = data; } } 

    然后你就能这么用了:

    [Test<int>(3)] [Test<float>(4.5f)] [Test<string>("hello")] 

    允许在方法上指定 AsyncMethodBuilder

    C# 10 将允许方法上使用 [AsyncMethodBuilder(...)] 来使用你自己实现的 async method builder,代替自带的 Task 或者 ValueTask 的异步方法构造器。这也有助于你自己实现零开销的异步方法。

    line 指示器支持行列和范围

    以前 #line 只能用来指定一个文件中的某一行,现在可以指定行列和范围了,这对写编译器和代码生成器的人非常有用:

    #line (startLine, startChar) - (endLine, endChar) charOffset "fileName" // 比如 #line (1, 1) - (2, 2) 3 "test.cs" 

    嵌套属性模式匹配改进

    以前在匹配嵌套属性的时候需要这么写:

    if (a is { X: { Y: { Z: 4 } } }) { ... } 

    现在只需要简单的:

    if (a is { X.Y.Z: 4 }) { ... } 

    就可以了

    改进的字符串插值

    以前 C# 的字符串插值是很粗暴的 string.Format,并且对于值类型参数来说会直接装箱,这不仅影响性能,用处也有限。现在字符串插值被改进了:

    var x = 1; Console.WriteLine($"hello, {x}"); 

    会被编译成:

    int x = 1; DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(7, 1); defaultInterpolatedStringHandler.AppendLiteral("hello, "); defaultInterpolatedStringHandler.AppendFormatted(x); Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear()); 

    上面这个 DefaultInterpolatedStringHandler 也可以借助 InterpolatedStringHandler 这个 attribute 替换成你自己实现的插值处理器,来决定要怎么进行插值。因此你甚至可以用来实现 SQL 的安全构建等等,功能性增强了很多。

    19 条回复    2021-09-02 00:21:09 +08:00
    youyouyou0123456
        1
    youyouyou0123456  
       2021-08-09 17:45:03 +08:00
    这次骚操作这么多。
    GM
        2
    GM  
       2021-08-09 17:55:06 +08:00   1
    太多东西了,还是我 Go 大道至简好 /狗头
    Rwing
        3
    Rwing  
       2021-08-09 18:04:34 +08:00
    hez 大佬
    netnr
        4
    netnr  
       2021-08-09 19:08:18 +08:00 via Android
    usings 最有用
    Youen
        5
    Youen  
       2021-08-09 19:47:40 +08:00 via iPhone
    C 井啊你慢一点,等等你的用户 狗头
    Removable
        6
    Removable  
       2021-08-09 21:06:13 +08:00 via iPhone
    啊公司的项目还在 7.0
    pcbl
        7
    pcbl  
       2021-08-09 21:14:52 +08:00
    搞这些,不如多搞一些通用性的基础库,更容易吸引开发者
    yejinmo
        8
    yejinmo  
       2021-08-09 21:37:18 +08:00
    太顶了学不过来了
    hez2010
        9
    hez2010  
    OP
       2021-08-09 22:08:22 +08:00
    @pcbl 基础库和语言完全是两个团队负责和更新的,相互并不冲突,况且 .NET 6 也确实给基础库引入了不少新的 API,只不过这篇是介绍语言的所以没有提。
    hez2010
        10
    hez2010  
    OP
       2021-08-09 22:11:13 +08:00
    @yejinmo 没必要一直跟着学,用到的时候花几分钟查一下文档就知道是怎么回事了。
    alexkkaa
        11
    alexkkaa  
       2021-08-09 22:39:00 +08:00 via Android
    dotnet 语言层面的特性已经太多了, 同一件事有很多种做法并不是一件好事,不如提供一种最优解。 这种现象在历史包袱很重的语言里经常见到。向 go 这种大道至简的其实是最好的, 一件事只有一种解法, 既降低了自己的心智负担也降低了别人阅读的难度,straight forward
    hez2010
        12
    hez2010  
    OP
       2021-08-09 23:03:56 +08:00   8
    @alexkkaa 提供一种最优解这件事本身就是不可能的,随着语言的演进原来的最优解将会不断变成非最优解,这个时候进行改进难道要把以前的东西砍掉吗?老项目、生态和兼容性怎么办? C# 现在内置了代码分析器,编写代码的时候会自动给出推荐用来自动将老代码翻新,可以把严重等级设置成“错误”那就等同于废弃了老的特性。

    而你提到的 go 倒不如说是矫枉过正,虽然目前只有一种解法,但却经常提供的是不好的解法,最典型的就是 go 的不少库实现居然在里面用反射枚举类型(如 fmt 等等),这也注定了这个库的性能和可扩展性都很低,这是典型的语言匮乏导致实现暴力效果还不好的例子。虽然 1.17 加了泛型但不好好做泛型约束和 sum types,却在做类型的枚举,扩展性没有改观;而非约束泛型 any T 底层仍然是 interface{},性能方面也没有任何改观。此外,鸭子类型本应该是 trait 理论诞生之前的临时替代品,go 诞生于 trait 出现之后的世界却大范围使用这套类型系统,不出意外出现了不少 interface 被意外实现的例子,于是又不得不在类型定义里面添加小写开头的函数或者 i() 来避免,这也本身也是一种类型系统设计失败的表现。那问题来了,除非 go 愿意从头错到尾,如果想要着手解决这些问题,那势必会引入新的特性来改进语言,一件事只有一种解法的状态本身也自然会被打破。
    kiracyan
        13
    kiracyan  
       2021-08-09 23:05:47 +08:00
    泛型特性是好东西
    waytoexplorewhat
        14
    waytoexplorewhat  
       2021-08-10 00:51:47 +08:00 via Android
    Haskell yyds
    jin7
        15
    jin7  
       2021-08-10 08:59:46 +08:00
    这个比 c++还复杂吗?
    hez2010
        16
    hez2010  
    OP
       2021-08-10 10:08:14 +08:00 via Android
    @jin7 这倒不至于,实际用起来就会发现还是很直观的。
    beyondex
        17
    beyondex  
       2021-08-11 21:49:13 +08:00
    棒极了,语法更加简洁精炼了。
    INCerry
        18
    INCerry  
       2021-08-12 16:40:03 +08:00
    @alexkkaa 不过看最近的提案来说 go 也在慢慢加回去
    RTSmile
        19
    RTSmile  
       2021-09-02 00:21:09 +08:00   1
    @alexkkaa Go 的大道至简其实是一种假象,用久了就感觉一切都是鬼扯。
    别看 C#一堆新东西看上去复杂,真到用的时候我可以用一行代码写出别人几行代码写出来的东西,性能甚至比别人更好。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     4688 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 23ms UTC 01:09 PVG 09:09 LAX 17:09 JFK 20:09
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86