...

C# 10 完整特性介紹

2021-08-13

前言

開(kāi)頭防杠:.NET 的基礎庫、語言、運行時(shí)團隊從來都(dōu)是相互獨立各自更新的,.NET 6 在基礎庫、運行時(shí)上同樣(yàng)做了非常多的改進(jìn),不過(guò)本文僅僅介紹語言部分。

距離上次介紹 C# 10 的特性已經(jīng)有一段時(shí)間了,伴随著(zhe) .NET 6 的開(kāi)發(fā)進(jìn)入尾聲,C# 10 最終的特性也終于敲定了。總的來說(shuō) C# 10 的更新内容很多,并且對(duì)類型系統做了不小的改動,解決了非常多現有的痛點。

從 C# 10 可以看到一個消息,那就(jiù)是 C# 語言團隊開(kāi)始主要著(zhe)重于改進(jìn)類型系統和功能(néng)性方面(miàn)的東西,而不是像以前那樣(yàng)熱衷于各種(zhǒng)語法糖了。C# 10 隻是這(zhè)個旅程的開(kāi)頭,後(hòu)面(miàn)的 C# 11 、12 將(jiāng)會(huì)有更多關于類型系統的改進(jìn),使其擁有強如 Haskell 、Rust 的表達能(néng)力,不僅能(néng)提供從頭到尾的跨程序集的靜态類型支持,還(hái)能(néng)做到像動态類型語言那樣(yàng)的靈活。邏輯代碼是類型的證明,隻有類型系統強大了,代碼編寫起(qǐ)來才能(néng)更順暢、更不容易出錯。

record struct

首先自然是 record struct,解決了 record 隻能(néng)給 class 而不能(néng)給 struct 用的問題:

recordstructPoint(intX,intY);

用 record 定義 struct 的好(hǎo)處其實有很多,例如你無需重寫 GetHashCode 和 Equals 之類的方法了。

sealed record ToString 方法

之前 record 的 ToString 是不能(néng)修飾爲 sealed 的,因此如果你繼承了一個 record,相應的 ToString 行爲也會(huì)被(bèi)改變,因此這(zhè)是個虛方法。

但是現在你可以把 record 裡(lǐ)的 ToString 方法标記成(chéng) sealed,這(zhè)樣(yàng)你的 ToString 方法就(jiù)不會(huì)被(bèi)重寫了。

struct 無參構造函數

一直以來 struct 不支持無參構造函數,現在支持了:

structFoo
{
   publicintX;
   publicFoo() { X = 1; }
}

但是使用的時(shí)候就(jiù)要注意了,因爲無參構造函數的存在使得 new struct() 和 default(struct) 的語義不一樣(yàng)了,例如 new Foo().X == default(Foo).X 在上面(miàn)這(zhè)個例子中將(jiāng)會(huì)得出 false

匿名對(duì)象的 with

可以用 with 來根據已有的匿名對(duì)象創建新的匿名對(duì)象了:

varx =new{ A = 1, B = 2 };
vary = xwith{ A = 3 };

這(zhè)裡(lǐ) y.A 將(jiāng)會(huì)是 3 。

全局的 using

利用全局 using 可以給整個項目啓用 usings,不再需要每個文件都(dōu)寫一份。比如你可以創建一個 Import.cs,然後(hòu)裡(lǐ)面(miàn)寫:

usingSystem;
usingi32 = System.Int32;

然後(hòu)你整個項目都(dōu)無需再 using System,并且可以用 i32 了。

文件範圍的 namespace

這(zhè)個比較簡單,以前寫 namespace 還(hái)得帶一層大括号,以後(hòu)如果一個文件裡(lǐ)隻有一個 namespace 的話,那直接在最上面(miàn)這(zhè)樣(yàng)寫就(jiù)行了:

namespaceMyNamespace;

常量字符串插值

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

conststringx ="hello";
conststringy =$"{x}, world!";

lambda 改進(jìn)

這(zhè)個改進(jìn)可以說(shuō)是非常大,我分多點介紹。

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 開(kāi)始允許在參數列表最前面(miàn)顯示指定 lambda 類型了:

f =int() => 4;

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

f =refint(refintx) =>refx;// 返回一個參數的引用

4. 頭等函數

函數可以隐式轉換到 delegate,于是函數上升至頭等函數:

voidFoo() { Console.WriteLine("hello"); }
varx = Foo;
x();// hello

5. 自然委托類型

lambda 現在會(huì)自動創建自然委托類型,于是不再需要寫出類型了。

varf = () => 1;// Func<int>
varg =string(intx,stringy) =>$"{y}{x}";// Func<int, string, string>
varh ="test".GetHashCode;// Func<int>

CallerArgumentExpression

現在,CallerArgumentExpression 這(zhè)個 attribute 終于有用了。借助這(zhè)個 attribute,編譯器會(huì)自動填充調用參數的表達式字符串,例如:

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

當你調用 Foo(4 + 5) 時(shí),會(huì)輸出 4 + 5 = 9。這(zhè)對(duì)測試框架極其有用,因爲你可以輸出 assert 的原表達式了:

staticvoidAssert(boolvalue, [CallerArgumentExpression("value")]string? expr =null)
{
   if(!value)thrownewAssertFailureException(expr);
}

tuple 支持混合定義和使用

比如:

inty = 0;
(varx, y,varz) = (1, 2, 3);

于是 y 就(jiù)變成(chéng) 2 了,同時(shí)還(hái)創建了兩(liǎng)個變量 x 和 z,分别是 1 和 3 。

接口支持抽象靜态方法

這(zhè)個特性將(jiāng)會(huì)在 .NET 6 作爲 preview 特性放出,意味著(zhe)默認是不啓用的,需要設置 <LangVersion>preview</LangVersion> 和 <EnablePreviewFeatures>true</EnablePreviewFeatures>,然後(hòu)引入一個官方的 nuget 包 System.Runtime.Experimental 來啓用。

然後(hòu)接口就(jiù)可以聲明抽象靜态成(chéng)員了,.NET 的類型系統正式具備虛靜态方法分發(fā)能(néng)力。

例如,你想定義一個可加而且有零的接口 IMonoid<T>

interfaceIMonoid<T>whereT:IMonoid<T>
{
   abstractstaticT Zero {get; }
   abstractstaticToperator+(T l, T r);
}

然後(hòu)可以對(duì)其進(jìn)行實現,例如這(zhè)裡(lǐ)的 MyInt:

publicclassMyInt:IMonoid<MyInt>
{
   publicMyInt(intval) { Value = val; }

   publicstaticMyInt Zero {get; } =newMyInt(0);
   publicstaticMyIntoperator+(MyInt l, MyInt r) =>newMyInt(l.Value + r.Value);

   publicintValue {get; }
}

然後(hòu)就(jiù)能(néng)寫出一個方法對(duì) IMoniod<T> 進(jìn)行求和了,這(zhè)裡(lǐ)爲了方便寫成(chéng)擴展方法:

publicstaticclassIMonoidExtensions
{
   publicstaticTSum<T>(thisIEnumerable<T> t)whereT : IMonoid<T>
   {
       varresult = T.Zero;
       foreach(variint) result += i;
       returnresult;
   }
}

最後(hòu)調用:

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

你可能(néng)會(huì)問爲什麼(me)要引入一個 System.Runtime.Experimental,因爲這(zhè)個包裡(lǐ)面(miàn)包含了 .NET 基礎類型的改進(jìn):給所有的基礎類型都(dōu)實現了相應的接口,比如給數值類型都(dōu)實現了 INumber<T>,給可以加的東西都(dōu)實現了 IAdditionOperators<TLeft, TRight, TResult> 等等,用起(qǐ)來將(jiāng)會(huì)非常方便,比如你想寫一個函數,這(zhè)個函數用來把能(néng)相加的東西加起(qǐ)來:

TAdd<T>(T left, T right)whereT : IAdditionOperators<T, T, T>
{
   returnleft + right;
}

就(jiù)搞定了。

接口的靜态抽象方法支持和未來 C# 將(jiāng)會(huì)加入的 shape 特性是相輔相成(chéng)的,屆時(shí) C# 將(jiāng)利用 interface 和 shape 支持 Haskell 的 class、Rust 的 trait 那樣(yàng)的 type classes,將(jiāng)類型系統上升到一個新的層次。

泛型 attribute

是的你沒(méi)有看錯,C# 的 attributes 支持泛型了:

classTestAttribute<T> :Attribute
{
   publicT Data {get; }
   publicTestAttribute(T data) { Data = data; }
}

然後(hòu)你就(jiù)能(néng)這(zhè)麼(me)用了:

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

允許在方法上指定 AsyncMethodBuilder

C# 10 將(jiāng)允許方法上使用 [AsyncMethodBuilder(...)] 來使用你自己實現的 async method builder,代替自帶的 Task 或者 ValueTask 的異步方法構造器。這(zhè)也有助于你自己實現零開(kāi)銷的異步方法。

line 指示器支持行列和範圍

以前 #line 隻能(néng)用來指定一個文件中的某一行,現在可以指定行列和範圍了,這(zhè)對(duì)寫編譯器和代碼生成(chéng)器的人非常有用:

#line (startLine, startChar) - (endLine, endChar) charOffset "fileName"

// 比如 #line (1, 1) - (2, 2) 3 "test.cs"

嵌套屬性模式匹配改進(jìn)

以前在匹配嵌套屬性的時(shí)候需要這(zhè)麼(me)寫:

if(ais{ X: { Y: { Z: 4 } } }) { ... }

現在隻需要簡單的:

if(ais{ X.Y.Z: 4 }) { ... }

就(jiù)可以了。

改進(jìn)的字符串插值

以前 C# 的字符串插值是很粗暴的 string.Format,并且對(duì)于值類型參數來說(shuō)會(huì)直接裝箱,對(duì)于多個參數而言還(hái)會(huì)因此而分配一個數組(比如 string.Format("{} {}", a, b) 其實是 string.Format("{} {}", new object [] { (object)a, (object)b })),這(zhè)很影響性能(néng)。現在字符串插值被(bèi)改進(jìn)了:

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

會(huì)被(bèi)編譯成(chéng):

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

上面(miàn)這(zhè)個 DefaultInterpolatedStringHandler 也可以借助 InterpolatedStringHandler 這(zhè)個 attribute 替換成(chéng)你自己實現的插值處理器,來決定要怎麼(me)進(jìn)行插值。借助這(zhè)些可以實現接近零開(kāi)銷的字符串插值。

Source Generator v2

代碼生成(chéng)器在 C# 10 將(jiāng)會(huì)迎來 v2 版本,這(zhè)個版本包含很多改進(jìn),包括強類型的代碼構建器,以及增量編譯的支持等等。


來源:DotNET技術圈