C# 10 完整特性介紹
前言
開頭防杠:.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)支持等等。