C# 10 完整特性介紹
前言
開(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),包括強類型的代碼構建器,以及增量編譯的支持等等。