...

C# 10 完整特性介紹

2021-08-13

前言

開頭防杠:.NET 的(de)基礎庫、語言、運行時(shí)團隊從來(lái)都是(shì)相互獨立各自更新的(de),.NET 6 在(zài)基礎庫、運行時(shí)上(shàng)同樣做了(le/liǎo)非常多的(de)改進,不(bù)過本文僅僅介紹語言部分。

距離上(shàng)次介紹 C# 10 的(de)特性已經有一(yī / yì /yí)段時(shí)間了(le/liǎo),伴随着 .NET 6 的(de)開發進入尾聲,C# 10 最終的(de)特性也(yě)終于(yú)敲定了(le/liǎo)。總的(de)來(lái)說(shuō) C# 10 的(de)更新内容很多,并且對類型系統做了(le/liǎo)不(bù)小的(de)改動,解決了(le/liǎo)非常多現有的(de)痛點。

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

record struct

首先自然是(shì) record struct,解決了(le/liǎo) record 隻能給 class 而(ér)不(bù)能給 struct 用的(de)問題:

recordstructPoint(intX,intY);

用 record 定義 struct 的(de)好處其實有很多,例如你無需重寫 GetHashCode 和(hé / huò) Equals 之(zhī)類的(de)方法了(le/liǎo)。

sealed record ToString 方法

之(zhī)前 record 的(de) ToString 是(shì)不(bù)能修飾爲(wéi / wèi) sealed 的(de),因此如果你繼承了(le/liǎo)一(yī / yì /yí)個(gè) record,相應的(de) ToString 行爲(wéi / wèi)也(yě)會被改變,因此這(zhè)是(shì)個(gè)虛方法。

但是(shì)現在(zài)你可以(yǐ)把 record 裏的(de) ToString 方法标記成 sealed,這(zhè)樣你的(de) ToString 方法就(jiù)不(bù)會被重寫了(le/liǎo)。

struct 無參構造函數

一(yī / yì /yí)直以(yǐ)來(lái) struct 不(bù)支持無參構造函數,現在(zài)支持了(le/liǎo):

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

但是(shì)使用的(de)時(shí)候就(jiù)要(yào / yāo)注意了(le/liǎo),因爲(wéi / wèi)無參構造函數的(de)存在(zài)使得 new struct() 和(hé / huò) default(struct) 的(de)語義不(bù)一(yī / yì /yí)樣了(le/liǎo),例如 new Foo().X == default(Foo).X 在(zài)上(shàng)面這(zhè)個(gè)例子(zǐ)中将會得出(chū) false

匿名對象的(de) with

可以(yǐ)用 with 來(lái)根據已有的(de)匿名對象創建新的(de)匿名對象了(le/liǎo):

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

這(zhè)裏 y.A 将會是(shì) 3 。

全局的(de) using

利用全局 using 可以(yǐ)給整個(gè)項目啓用 usings,不(bù)再需要(yào / yāo)每個(gè)文件都寫一(yī / yì /yí)份。比如你可以(yǐ)創建一(yī / yì /yí)個(gè) Import.cs,然後裏面寫:

usingSystem;
usingi32 = System.Int32;

然後你整個(gè)項目都無需再 using System,并且可以(yǐ)用 i32 了(le/liǎo)。

文件範圍的(de) namespace

這(zhè)個(gè)比較簡單,以(yǐ)前寫 namespace 還得帶一(yī / yì /yí)層大(dà)括号,以(yǐ)後如果一(yī / yì /yí)個(gè)文件裏隻有一(yī / yì /yí)個(gè) namespace 的(de)話,那直接在(zài)最上(shàng)面這(zhè)樣寫就(jiù)行了(le/liǎo):

namespaceMyNamespace;

常量字符串插值

你可以(yǐ)給 const string 使用字符串插值了(le/liǎo),非常方便:

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

lambda 改進

這(zhè)個(gè)改進可以(yǐ)說(shuō)是(shì)非常大(dà),我分多點介紹。

1. 支持 attributes

lambda 可以(yǐ)帶 attribute 了(le/liǎo):

f = [Foo] (x) => x;// 給 lambda 設置
f = [return: Foo] (x) => x;// 給 lambda 返回值設置
f = ([Foo] x) => x;// 給 lambda 參數設置

2. 支持指定返回值類型

此前 C# 的(de) lambda 返回值類型靠推導,C# 10 開始允許在(zài)參數列表最前面顯示指定 lambda 類型了(le/liǎo):

f =int() => 4;

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

f =refint(refintx) =>refx;// 返回一(yī / yì /yí)個(gè)參數的(de)引用

4. 頭等函數

函數可以(yǐ)隐式轉換到(dào) delegate,于(yú)是(shì)函數上(shàng)升至頭等函數:

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

5. 自然委托類型

lambda 現在(zài)會自動創建自然委托類型,于(yú)是(shì)不(bù)再需要(yào / yāo)寫出(chū)類型了(le/liǎo)。

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

CallerArgumentExpression

現在(zài),CallerArgumentExpression 這(zhè)個(gè) attribute 終于(yú)有用了(le/liǎo)。借助這(zhè)個(gè) attribute,編譯器會自動填充調用參數的(de)表達式字符串,例如:

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

當你調用 Foo(4 + 5) 時(shí),會輸出(chū) 4 + 5 = 9。這(zhè)對測試框架極其有用,因爲(wéi / wèi)你可以(yǐ)輸出(chū) assert 的(de)原表達式了(le/liǎo):

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

tuple 支持混合定義和(hé / huò)使用

比如:

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

于(yú)是(shì) y 就(jiù)變成 2 了(le/liǎo),同時(shí)還創建了(le/liǎo)兩個(gè)變量 x 和(hé / huò) z,分别是(shì) 1 和(hé / huò) 3 。

接口支持抽象靜态方法

這(zhè)個(gè)特性将會在(zài) .NET 6 作爲(wéi / wèi) preview 特性放出(chū),意味着默認是(shì)不(bù)啓用的(de),需要(yào / yāo)設置 <LangVersion>preview</LangVersion> 和(hé / huò) <EnablePreviewFeatures>true</EnablePreviewFeatures>,然後引入一(yī / yì /yí)個(gè)官方的(de) nuget 包 System.Runtime.Experimental 來(lái)啓用。

然後接口就(jiù)可以(yǐ)聲明抽象靜态成員了(le/liǎo),.NET 的(de)類型系統正式具備虛靜态方法分發能力。

例如,你想定義一(yī / yì /yí)個(gè)可加而(ér)且有零的(de)接口 IMonoid<T>

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

然後可以(yǐ)對其進行實現,例如這(zhè)裏的(de) 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; }
}

然後就(jiù)能寫出(chū)一(yī / yì /yí)個(gè)方法對 IMoniod<T> 進行求和(hé / huò)了(le/liǎo),這(zhè)裏爲(wéi / wèi)了(le/liǎo)方便寫成擴展方法:

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

最後調用:

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

你可能會問爲(wéi / wèi)什麽要(yào / yāo)引入一(yī / yì /yí)個(gè) System.Runtime.Experimental,因爲(wéi / wèi)這(zhè)個(gè)包裏面包含了(le/liǎo) .NET 基礎類型的(de)改進:給所有的(de)基礎類型都實現了(le/liǎo)相應的(de)接口,比如給數值類型都實現了(le/liǎo) INumber<T>,給可以(yǐ)加的(de)東西都實現了(le/liǎo) IAdditionOperators<TLeft, TRight, TResult> 等等,用起來(lái)将會非常方便,比如你想寫一(yī / yì /yí)個(gè)函數,這(zhè)個(gè)函數用來(lái)把能相加的(de)東西加起來(lái):

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

就(jiù)搞定了(le/liǎo)。

接口的(de)靜态抽象方法支持和(hé / huò)未來(lái) C# 将會加入的(de) shape 特性是(shì)相輔相成的(de),屆時(shí) C# 将利用 interface 和(hé / huò) shape 支持 Haskell 的(de) class、Rust 的(de) trait 那樣的(de) type classes,将類型系統上(shàng)升到(dào)一(yī / yì /yí)個(gè)新的(de)層次。

泛型 attribute

是(shì)的(de)你沒有看錯,C# 的(de) attributes 支持泛型了(le/liǎo):

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

然後你就(jiù)能這(zhè)麽用了(le/liǎo):

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

允許在(zài)方法上(shàng)指定 AsyncMethodBuilder

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

line 指示器支持行列和(hé / huò)範圍

以(yǐ)前 #line 隻能用來(lái)指定一(yī / yì /yí)個(gè)文件中的(de)某一(yī / yì /yí)行,現在(zài)可以(yǐ)指定行列和(hé / huò)範圍了(le/liǎo),這(zhè)對寫編譯器和(hé / huò)代碼生成器的(de)人(rén)非常有用:

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

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

嵌套屬性模式匹配改進

以(yǐ)前在(zài)匹配嵌套屬性的(de)時(shí)候需要(yào / yāo)這(zhè)麽寫:

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

現在(zài)隻需要(yào / yāo)簡單的(de):

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

就(jiù)可以(yǐ)了(le/liǎo)。

改進的(de)字符串插值

以(yǐ)前 C# 的(de)字符串插值是(shì)很粗暴的(de) string.Format,并且對于(yú)值類型參數來(lái)說(shuō)會直接裝箱,對于(yú)多個(gè)參數而(ér)言還會因此而(ér)分配一(yī / yì /yí)個(gè)數組(比如 string.Format("{} {}", a, b) 其實是(shì) string.Format("{} {}", new object [] { (object)a, (object)b })),這(zhè)很影響性能。現在(zài)字符串插值被改進了(le/liǎo):

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

會被編譯成:

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

上(shàng)面這(zhè)個(gè) DefaultInterpolatedStringHandler 也(yě)可以(yǐ)借助 InterpolatedStringHandler 這(zhè)個(gè) attribute 替換成你自己實現的(de)插值處理器,來(lái)決定要(yào / yāo)怎麽進行插值。借助這(zhè)些可以(yǐ)實現接近零開銷的(de)字符串插值。

Source Generator v2

代碼生成器在(zài) C# 10 将會迎來(lái) v2 版本,這(zhè)個(gè)版本包含很多改進,包括強類型的(de)代碼構建器,以(yǐ)及增量編譯的(de)支持等等。


來(lái)源:DotNET技術圈