V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
hez2010
V2EX  ›  C#

C# 10 完整特性介绍

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

    一直忘了 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   ·   实用小工具   ·   3455 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 04:59 · PVG 12:59 · LAX 20:59 · JFK 23:59
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.