海南指甲叶染发 發表於 2021-8-12 16:35:00

C# 10 完整特性介绍

<h2 id="前言">前言</h2>
<p>开头防杠:.NET 的基础库、语言、运行时团队从来都是相互独立各自更新的,.NET 6 在基础库、运行时上同样做了非常多的改进,不过本文仅仅介绍语言部分。</p>
<p>距离上次介绍 C# 10 的特性已经有一段时间了,伴随着 .NET 6 的开发进入尾声,C# 10 最终的特性也终于敲定了。总的来说 C# 10 的更新内容很多,并且对类型系统做了不小的改动,解决了非常多现有的痛点。</p>
<p>从 C# 10 可以看到一个消息,那就是 C# 语言团队开始主要着重于改进类型系统和功能性方面的东西,而不是像以前那样热衷于各种语法糖了。C# 10 只是这个旅程的开头,后面的 C# 11 、12 将会有更多关于类型系统的改进,使其拥有强如 Haskell 、Rust 的表达能力,不仅能提供从头到尾的跨程序集的静态类型支持,还能做到像动态类型语言那样的灵活。逻辑代码是类型的证明,只有类型系统强大了,代码编写起来才能更顺畅、更不容易出错。</p>
<h2 id="record-struct">record struct</h2>
<p>首先自然是 record struct,解决了 record 只能给 class 而不能给 struct 用的问题:</p>
<pre><code class="language-csharp">record struct Point(int X, int Y);
</code></pre>
<p>用 record 定义 struct 的好处其实有很多,例如你无需重写 <code>GetHashCode</code> 和 <code>Equals</code> 之类的方法了。</p>
<h2 id="sealed-record-tostring-方法">sealed record <code>ToString</code> 方法</h2>
<p>之前 record 的 ToString 是不能修饰为 <code>sealed</code> 的,因此如果你继承了一个 record,相应的 ToString 行为也会被改变,因此这是个虚方法。</p>
<p>但是现在你可以把 record 里的 ToString 方法标记成 <code>sealed</code>,这样你的 <code>ToString</code> 方法就不会被重写了。</p>
<h2 id="struct-无参构造函数">struct 无参构造函数</h2>
<p>一直以来 struct 不支持无参构造函数,现在支持了:</p>
<pre><code class="language-csharp">struct Foo
{
    public int X;
    public Foo() { X = 1; }
}
</code></pre>
<p>但是使用的时候就要注意了,因为无参构造函数的存在使得 <code>new struct()</code> 和 <code>default(struct)</code> 的语义不一样了,例如 <code>new Foo().X == default(Foo).X</code> 在上面这个例子中将会得出 <code>false</code>。</p>
<h2 id="匿名对象的-with">匿名对象的 with</h2>
<p>可以用 with 来根据已有的匿名对象创建新的匿名对象了:</p>
<pre><code class="language-csharp">var x = new { A = 1, B = 2 };
var y = x with { A = 3 };
</code></pre>
<p>这里 <code>y.A</code> 将会是 3 。</p>
<h2 id="全局的-using">全局的 using</h2>
<p>利用全局 using 可以给整个项目启用 usings,不再需要每个文件都写一份。比如你可以创建一个 Import.cs,然后里面写:</p>
<pre><code class="language-csharp">using System;
using i32 = System.Int32;
</code></pre>
<p>然后你整个项目都无需再 <code>using System</code>,并且可以用 <code>i32</code> 了。</p>
<h2 id="文件范围的-namespace">文件范围的 namespace</h2>
<p>这个比较简单,以前写 namespace 还得带一层大括号,以后如果一个文件里只有一个 namespace 的话,那直接在最上面这样写就行了:</p>
<pre><code class="language-csharp">namespace MyNamespace;
</code></pre>
<h2 id="常量字符串插值">常量字符串插值</h2>
<p>你可以给 const string 使用字符串插值了,非常方便:</p>
<pre><code class="language-csharp">const string x = "hello";
const string y = $"{x}, world!";
</code></pre>
<h2 id="lambda-改进">lambda 改进</h2>
<p>这个改进可以说是非常大,我分多点介绍。</p>
<h3 id="1-支持-attributes">1. 支持 attributes</h3>
<p>lambda 可以带 attribute 了:</p>
<pre><code class="language-csharp">f = (x) =&gt; x; // 给 lambda 设置
f = (x) =&gt; x; // 给 lambda 返回值设置
f = ( x) =&gt; x; // 给 lambda 参数设置
</code></pre>
<h3 id="2-支持指定返回值类型">2. 支持指定返回值类型</h3>
<p>此前 C# 的 lambda 返回值类型靠推导,C# 10 开始允许在参数列表最前面显示指定 lambda 类型了:</p>
<pre><code class="language-csharp">f = int () =&gt; 4;
</code></pre>
<h3 id="3-支持-ref-in-out-等修饰">3. 支持 ref 、in 、out 等修饰</h3>
<pre><code class="language-csharp">f = ref int (ref int x) =&gt; ref x; // 返回一个参数的引用
</code></pre>
<h3 id="4-头等函数">4. 头等函数</h3>
<p>函数可以隐式转换到 delegate,于是函数上升至头等函数:</p>
<pre><code class="language-csharp">void Foo() { Console.WriteLine("hello"); }
var x = Foo;
x(); // hello
</code></pre>
<h3 id="5-自然委托类型">5. 自然委托类型</h3>
<p>lambda 现在会自动创建自然委托类型,于是不再需要写出类型了。</p>
<pre><code class="language-csharp">var f = () =&gt; 1; // Func&lt;int&gt;
var g = string (int x, string y) =&gt; $"{y}{x}"; // Func&lt;int, string, string&gt;
var h = "test".GetHashCode; // Func&lt;int&gt;
</code></pre>
<h2 id="callerargumentexpression">CallerArgumentExpression</h2>
<p>现在,<code>CallerArgumentExpression</code> 这个 attribute 终于有用了。借助这个 attribute,编译器会自动填充调用参数的表达式字符串,例如:</p>
<pre><code class="language-csharp">void Foo(int value, string? expression = null)
{
    Console.WriteLine(expression + " = " + value);
}
</code></pre>
<p>当你调用 <code>Foo(4 + 5)</code> 时,会输出 <code>4 + 5 = 9</code>。这对测试框架极其有用,因为你可以输出 assert 的原表达式了:</p>
<pre><code class="language-csharp">static void Assert(bool value, string? expr = null)
{
    if (!value) throw new AssertFailureException(expr);
}
</code></pre>
<h2 id="tuple-支持混合定义和使用">tuple 支持混合定义和使用</h2>
<p>比如:</p>
<pre><code class="language-csharp">int y = 0;
(var x, y, var z) = (1, 2, 3);
</code></pre>
<p>于是 y 就变成 2 了,同时还创建了两个变量 x 和 z,分别是 1 和 3 。</p>
<h2 id="接口支持抽象静态方法">接口支持抽象静态方法</h2>
<p>这个特性将会在 .NET 6 作为 preview 特性放出,意味着默认是不启用的,需要设置 <code>&lt;LangVersion&gt;preview&lt;/LangVersion&gt;</code> 和 <code>&lt;EnablePreviewFeatures&gt;true&lt;/EnablePreviewFeatures&gt;</code>,然后引入一个官方的 nuget 包 <code>System.Runtime.Experimental</code> 来启用。</p>
<p>然后接口就可以声明抽象静态成员了,.NET 的类型系统正式具备虚静态方法分发能力。</p>
<p>例如,你想定义一个可加而且有零的接口 <code>IMonoid&lt;T&gt;</code>:</p>
<pre><code class="language-csharp">interface IMonoid&lt;T&gt; where T : IMonoid&lt;T&gt;
{
    abstract static T Zero { get; }
    abstract static T operator+(T l, T r);
}
</code></pre>
<p>然后可以对其进行实现,例如这里的 MyInt:</p>
<pre><code class="language-csharp">public class MyInt : IMonoid&lt;MyInt&gt;
{
    public MyInt(int val) { Value = val; }

    public static MyInt Zero { get; } = new MyInt(0);
    public static MyInt operator+(MyInt l, MyInt r) =&gt; new MyInt(l.Value + r.Value);

    public int Value { get; }
}
</code></pre>
<p>然后就能写出一个方法对 <code>IMoniod&lt;T&gt;</code> 进行求和了,这里为了方便写成扩展方法:</p>
<pre><code class="language-csharp">public static class IMonoidExtensions
{
    public static T Sum&lt;T&gt;(this IEnumerable&lt;T&gt; t) where T : IMonoid&lt;T&gt;
    {
      var result = T.Zero;
      foreach (var i in t) result += i;
      return result;
    }
}
</code></pre>
<p>最后调用:</p>
<pre><code class="language-csharp">List&lt;MyInt&gt; list = new() { new(1), new(2), new(3) };
Console.WriteLine(list.Sum().Value); // 6
</code></pre>
<p>你可能会问为什么要引入一个 <code>System.Runtime.Experimental</code>,因为这个包里面包含了 .NET 基础类型的改进:给所有的基础类型都实现了相应的接口,比如给数值类型都实现了 <code>INumber&lt;T&gt;</code>,给可以加的东西都实现了 <code>IAdditionOperators&lt;TLeft, TRight, TResult&gt;</code> 等等,用起来将会非常方便,比如你想写一个函数,这个函数用来把能相加的东西加起来:</p>
<pre><code class="language-csharp">T Add&lt;T&gt;(T left, T right) where T : IAdditionOperators&lt;T, T, T&gt;
{
    return left + right;
}
</code></pre>
<p>就搞定了。</p>
<p>接口的静态抽象方法支持和未来 C# 将会加入的 shape 特性是相辅相成的,届时 C# 将利用 interface 和 shape 支持 Haskell 的 <code>class</code>、Rust 的 <code>trait</code> 那样的 type classes,将类型系统上升到一个新的层次。</p>
<h2 id="泛型-attribute">泛型 attribute</h2>
<p>是的你没有看错,C# 的 attributes 支持泛型了,不过 .NET 6 中将以预览特定放出,因此需要 <code>&lt;LangVersion&gt;preview&lt;/LangVersion&gt;</code>:</p>
<pre><code class="language-csharp">class TestAttribute&lt;T&gt; : Attribute
{
    public T Data { get; }
    public TestAttribute(T data) { Data = data; }
}
</code></pre>
<p>然后你就能这么用了:</p>
<pre><code class="language-csharp">


</code></pre>
<h2 id="允许在方法上指定-asyncmethodbuilder">允许在方法上指定 AsyncMethodBuilder</h2>
<p>C# 10 将允许方法上使用 <code></code> 来使用你自己实现的 async method builder,代替自带的 <code>Task</code> 或者 <code>ValueTask</code> 的异步方法构造器。这也有助于你自己实现零开销的异步方法。</p>
<h2 id="line-指示器支持行列和范围">line 指示器支持行列和范围</h2>
<p>以前 <code>#line</code> 只能用来指定一个文件中的某一行,现在可以指定行列和范围了,这对写编译器和代码生成器的人非常有用:</p>
<pre><code class="language-csharp">#line (startLine, startChar) - (endLine, endChar) charOffset "fileName"

// 比如 #line (1, 1) - (2, 2) 3 "test.cs"
</code></pre>
<h2 id="嵌套属性模式匹配改进">嵌套属性模式匹配改进</h2>
<p>以前在匹配嵌套属性的时候需要这么写:</p>
<pre><code class="language-csharp">if (a is { X: { Y: { Z: 4 } } }) { ... }
</code></pre>
<p>现在只需要简单的:</p>
<pre><code class="language-csharp">if (a is { X.Y.Z: 4 }) { ... }
</code></pre>
<p>就可以了。</p>
<h2 id="改进的字符串插值">改进的字符串插值</h2>
<p>以前 C# 的字符串插值是很粗暴的 string.Format,并且对于值类型参数来说会直接装箱,对于多个参数而言还会因此而分配一个数组(比如 <code>string.Format("{} {}", a, b)</code> 其实是 <code>string.Format("{} {}", new object [] { (object)a, (object)b })</code>),这很影响性能。现在字符串插值被改进了:</p>
<pre><code class="language-csharp">var x = 1;
Console.WriteLine($"hello, {x}");
</code></pre>
<p>会被编译成:</p>
<pre><code class="language-csharp">int x = 1;
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(7, 1);
defaultInterpolatedStringHandler.AppendLiteral("hello, ");
defaultInterpolatedStringHandler.AppendFormatted(x);
Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear());
</code></pre>
<p>上面这个 <code>DefaultInterpolatedStringHandler</code> 也可以借助 <code>InterpolatedStringHandler</code> 这个 attribute 替换成你自己实现的插值处理器,来决定要怎么进行插值。借助这些可以实现接近零开销的字符串插值。</p>
<h2 id="source-generator-v2">Source Generator v2</h2>
<p>代码生成器在 C# 10 将会迎来 v2 版本,这个版本包含很多改进,包括强类型的代码构建器,以及增量编译的支持等等。</p><br><br>
来源:https://www.cnblogs.com/hez2010/p/whats-new-in-csharp-10.html
頁: [1]
查看完整版本: C# 10 完整特性介绍