C# 對類型系統(tǒng)擴展性的改進
我們從最初的最初開始說起。接口(interface)在 C# 的類型系統(tǒng)中是一個非常關(guān)鍵的部分,用來對行為進行抽象,例如可以抽象“能被從字符串解析為整數(shù)”這件事情的接口可以定義為:
interface?IIntParsable
{
????int?Parse(string?text);
}
這樣一切實現(xiàn)了該接口的類型就可以直接轉(zhuǎn)換為?IIntParsable,然后調(diào)用其?Parse?方法把?string?解析成?int,用來根據(jù)字符串來創(chuàng)建整數(shù):
class?IntFactory?:?IIntParsable
{
????public?int?Parse(string?text)?{?...?}
}
但是這樣顯然通用性不夠,如果我們不想創(chuàng)建?int,而是想創(chuàng)建其他類型的實例的話,就需要定義無數(shù)個類型不同而抽象的事情相同的接口,或者將?Parse?的返回值改成?object,這樣就能通用了,但是對值類型會造成裝箱和拆箱導(dǎo)致性能問題,并且調(diào)用方也無法在編譯時知道?Parse?出來的到底是個什么類型的東西。
泛型接口
為了解決上面的這一問題,C# 進一步引入了泛型。泛型的引入允許接口定義類型參數(shù),因此對于上面的接口而言,不再需要為不同類型重復(fù)定義接口,而只需要定義一個泛型接口即可:
interface?IParsable<T>
{
????T?Parse(string?text);
}
這樣,當一個類型需要實現(xiàn)?IParsable?時,就可以這么實現(xiàn)了:
class?IntFactory?:?IParsable<int>
{
????public?int?Parse(string?text)?{?...?}
}
由此,我們誕生了各式各樣的工廠,例如上面這個?IntFactory?用來根據(jù)?string?來創(chuàng)建?int。
基于這些東西,甚至發(fā)展出了一個專門的工廠模式。但是這么做還有一個問題,假如我在接口中添加了一個新的方法?Foo,那么所有實現(xiàn)了這個接口的類型就不得不實現(xiàn)這個新的?Foo,否則會造成編譯失敗。
接口的方法默認實現(xiàn)
為了解決上述問題,C# 為接口引入了默認接口實現(xiàn),允許用戶為接口添加默認的方法實現(xiàn)。有了默認實現(xiàn)之后,即使開發(fā)者為一個接口添加了新的方法,只要提供一個默認實現(xiàn),就不會導(dǎo)致類型錯誤而編譯失?。?/span>
interface?IParsable<T>
{
????T?Parse(string?text);
????public?void?Foo()?{?...?}
}
這樣一來,IParsable?就有?Foo?方法了。不過要注意的是,這個?Foo?方法不同于?Parse?方法,Foo?如果沒有被實現(xiàn),則不是虛方法,也就是說它的實現(xiàn)在接口上,而不會帶到?jīng)]有實現(xiàn)這個接口的類上。如果不給類實現(xiàn)?Foo?無法調(diào)用的,除非把類型強制轉(zhuǎn)換到接口上:
class?IntFactory?:?IParsable<int>
{
????public?int?Parse(string?text)?{?...?}
}
interface?IParsable<T>
{
????T?Parse(string?text);
????public?void?Foo()?{?...?}
}
var?parser?=?new?IntFactory();
parser.Foo();?//?錯誤
((IParsable<int>)parser).Foo();?//?沒問題
接口的靜態(tài)方法默認實現(xiàn)
既然接口能默認實現(xiàn)方法了,那擴充一下讓接口支持實現(xiàn)靜態(tài)方法也是沒有問題的:
interface?IParsable<T>
{
????T?Parse(string?text);
????public?void?Foo()?{?...?}
????public?static?void?Bar()?{?...?}
}
不過,接口中的這樣的靜態(tài)方法同樣不是虛方法,只有在接口上才能進行調(diào)用,并且也不能被其他類型實現(xiàn)。跟類中的靜態(tài)方法一樣,想要調(diào)用的時候,只需要:
IParsable<int>.Bar();
即可。你可能會好奇這個和多繼承有什么區(qū)別,C# 中接口的默認實現(xiàn)都是非虛的,并且還無法訪問字段和不公開的方法,只當作一個向前兼容的設(shè)施即可,因此不必擔心 C++ 的多繼承問題會出現(xiàn)在 C# 里面。
接口的虛靜態(tài)方法
將接口的靜態(tài)方法作為非虛方法顯然有一定的局限性:
只能在接口上調(diào)用靜態(tài)方法,卻不能在實現(xiàn)了接口的類上調(diào)用,實用性不高
類沒法重寫接口靜態(tài)方法的實現(xiàn),進而沒法用來抽象運算符重載和各類工廠方法
因此,從 C# 10 開始,引入了抽象/虛靜態(tài)方法的概念,允許接口定義抽象靜態(tài)方法;在 C# 11 中則會允許定義虛靜態(tài)方法。這樣一來,之前的?IParsable?的例子中,我們就可以改成:
interface?IParsable<T>
{
????abstract?static?T?Parse(string?text);
}
然后我們可以對該接口進行實現(xiàn):
struct?Int32?:?IParsable
{
????public?static?int?Parse(string?text)?{?...?}
}
如此一來,我們組合泛型約束,誕生了一種全新的設(shè)計模式完全代替了原來需要創(chuàng)建工廠實例的工廠模式:
T?CreateInstance(string?text)?where?T?:?IParsable
{
????return?T.Parse(text);
}
原來需要專門寫一個工廠類型來做的事情,現(xiàn)在只需要一個函數(shù)就能完成同樣甚至更強大的功能,不僅能省掉工廠自身的分配,編寫起來也更加簡單了,并且還能用到運算符上!原本的工廠模式被我們徹底扔進垃圾桶。
我們還可以將各種接口組合起來應(yīng)用在泛型參數(shù)上,例如我們想編寫一個通用的方法用來計算?a * b + c,但是我們不知道其類型,現(xiàn)在只需要簡單的:
V?Calculate(T?a,?U?b,?V?c)
????where?T?:?IMultiplyOperators
????where?U?:?IAdditionOperators
{
????return?a?*?b?+?c;
}
其中?IAdditionOperators?和?IMultiplyOperators?都是 .NET 7 自帶的接口,三個類型參數(shù)分別是左操作數(shù)類型、右操作數(shù)類型和返回值類型,并且給所有可以實現(xiàn)的自帶類型都實現(xiàn)了。于是我們調(diào)用的時候只需要簡單的?Calculate(1, 2, 3)?就能得到?5;而如果是?Calculate(1.0, 1.5, 2.0)?則可以得到?3.5。
角色和擴展
至此,接口自身的演進就已經(jīng)完成了。接下來就是 C# 的下一步計劃:改進類型系統(tǒng)的擴展性。下面的東西預(yù)計會在接下來的幾年(C# 12 或者之后)到來。C# 此前一直是一門面向?qū)ο笳Z言,因此擴展性當然可以通過繼承和多態(tài)來做到,但是這么做有很大的問題:
繼承理論本身的問題:例如根據(jù)繼承原則,正方形類型繼承自長方形,而長方形又繼承自四邊形,但是長方形其實不需要獨立的四邊長度、正方形也不存在長寬的說法,這造成了實現(xiàn)上的冗余和定義上的不準確
對類而言,只有單繼承,沒法將多個父類組合起來繼承到自類上
與值類型不兼容,因為值類型不支持繼承
對接口而言,雖然類型可以實現(xiàn)多個接口,但是如果要為一個類型添加新的接口,則需要修改類型原來的定義,而無法進行擴展
最初為了支持給類型擴展新的方法,C# 引入了擴展方法功能,滿足了大多數(shù)情況的使用,但是局限性很大:
擴展方法只能是靜態(tài)方法,無法訪問被擴展類型內(nèi)部的私有成員
擴展方法不支持索引器,也不支持屬性,更不支持運算符
社區(qū)中也一直存在不少意見希望能讓 C# 支持擴展一切,C# 8 的時候官方還實現(xiàn)了這個功能,但是最終在發(fā)布之前砍掉了。
為什么?因為有了更好和更通用的做法。既然我們已經(jīng)有了以上對接口的改進,我們何必再去給一個局限性很大的擴展方法縫縫補補呢?因此,角色和擴展誕生了。
在這個模式里,接口將成為核心,同時徹底拋棄了繼承。接口由于自身的特點,在 C# 中也天然成為了 Rust 中?dyn trait?以及 Haskell 中?type class?的等價物。
注意:以下的東西目前都處于設(shè)計階段,因此下述內(nèi)容只是對目前設(shè)計的介紹,最終的設(shè)計和實現(xiàn)可能會隨著對相關(guān)特性的進一步討論而發(fā)生變化,但是總體方向不會變。
角色
一個角色在 C# 中可以采用如下方式定義:
role?Name?:?UnderlyingType,?Interface,?...?where?T?:?Constraint
這樣一來,如果我們想給一個已有的類型?Foo?實現(xiàn)一個有著接口?IBar?的角色,我們就可以這么寫:
role?Bar?:?Foo,?IBar?{?...?}
這樣我們就創(chuàng)建了一個角色?Bar,這個?Bar?則只實現(xiàn)了?IBar,而不會暴露?Foo?中的其他成員。且不同于繼承,Foo?和?Bar?本質(zhì)上是同一個類型,只是擁有著不同的角色,他們之前可以相互轉(zhuǎn)換。舉一些現(xiàn)實的例子,假設(shè)我們有一個接口?IPerson:
interface?IPerson
{
????int?Id?{?get;?}
????string?Name?{?get;?}
????int?Age?{?get;?}
}
然后我們有一個類型?Data?使用字典存儲了很多數(shù)據(jù),并且?Data?自身具有一個?Id:
class?Data
{
????public?int?Id?{?get;?}
????public?Dictionary<string,?string>?Values?{?get;?}?=??...;
}
那我們就可以給?Data?創(chuàng)建一個?Person?的角色:
role?Person?:?Data,?IPerson
{
????public?string?Name?=>?this.Values["name"];
????public?int?Age?=>?int.Parse(this.Values["age"]);
}
其中,無需實現(xiàn)?Id,因為它已經(jīng)在?Data?中包含了。最終,這個?Person?就是一個只實現(xiàn)了?IPerson?的?Data,它只暴露了?Id、Name?和?Age?屬性,而不會暴露來自?Data?的?Values?屬性。
以及,它可以被傳到任何接受?Person、Data?或者?IPerson?的地方。我們還可以組合多個接口來創(chuàng)建這樣的角色,例如:
interface?IHasAge
{
????int?Age?{?get;?}
}
interface?IHasName
{
????string?Name?{?get;?}
}
role?Person?:?Data,?IHasAge,?IHasName
{
????//?...
}
這樣我們把?IPerson?拆成了?IHasAge?和?IHasName?的組合。另外,在不實現(xiàn)接口的情況下,角色也可以用來作為類型的輕量級封裝:
role?Person?:?Data
{
????public?string?Name?=>?this.Values["name"];
????public?int?Age?=>?int.Parse(this.Values["age"]);
}
如此一來,Person?將成為一種提供以“人”的方式訪問?Data?的方法的類型??梢哉f,角色就是對同一個“data”的不同的“view”,一個類型的所有角色和它自身都是同樣的類型,在本質(zhì)上和繼承是完全不同的!與其他語言的概念類比的話,角色就等同于 concepts,這也意味著 C# 向 structural typing 邁出了一大步。
擴展
有了角色之后,為了解決擴展性的問題,C# 將會引入擴展。有時候我們不想通過角色來訪問一個對象里的東西,我們可以直接在外部擴展已有的類型。
extension?DataExtension?:?Data
{
????public?string?Name?=>?this.Values["name"];
????public?string?ToJson()?{?...?}
}
這樣,Data?類型就有了名為?Name?的屬性和?ToJson?的方法,可以直接調(diào)用。除了屬性和方法之外,擴展一個索引器自然也不在話下。
其中的?ToJson?類似以前的擴展方法,不過如此一來,以前 C# 的擴展方法特性已經(jīng)徹底被新的擴展特性取代,而且是上位替代,功能性和靈活性上遠超原來的擴展方法。我們還可以給類型擴展實現(xiàn)接口:
extension?DataExtension?:?Data,?IHasName
{
????public?string?Name?=>?this.Values["name"];
}
這樣一來,Data?就實現(xiàn)了?IHasName,可以傳遞到任何接受?IHasName?的地方。甚至借助接口的虛靜態(tài)方法和泛型,我們可以給所有的整數(shù)類型擴展一個遍歷器,用來按字節(jié)遍歷底層的表示:
extension?ByteEnumerator?:?T,?IEnumerable<byte>?where?T?:?unmanaged,?IShiftOperators
{
????public?IEnumerator<byte>?GetEnumerator()
????{
????????for?(var?i?=?sizeof(T);?i?>?0;?i--)
????????{
????????????yield?return?unchecked((byte)this?>>?((i?-?1)?*?8));
????????}
????}
}
foreach?(var?b?in?11223344556677L)
{
????Console.WriteLine(b);
}
配合接口的靜態(tài)方法,我們甚至能給已有的類型擴展實現(xiàn)運算符!
extension?MyExtension?:?Foo,?IAdditionOperators
{
????public?static?Foo?operator+(Foo?left,?Foo?right)?{?...?}
}
var?foo1?=?new?Foo(...);
var?foo2?=?new?Foo(...);
var?result?=?foo1?+?foo2;
總結(jié)
C# 從 8 版本開始逐漸開始對接口進行操刀,最終的目的其實就是為了實現(xiàn)角色和擴展,改善類型系統(tǒng)的擴展性。
到了 C# 11,C# 對接口部分的改造已經(jīng)全部完成,接下來就是角色和擴展了。當然,目前還為時尚早,具體的設(shè)計和實現(xiàn)也可能會變化。最終,借助接口、泛型、角色和擴展,C# 的類型系統(tǒng)將擁有等同于 Haskell 的?type class?那樣的強大表達力和擴展性。
而且由于是靜態(tài)類型,從頭到尾都不需要擔心任何的類型安全問題。也可以預(yù)想到,隨著這些特性的推出,將會有不少已有的設(shè)計模式因為有了更好的做法而被取代和淘汰。
轉(zhuǎn)自:hez2010
鏈接:zhuanlan.zhihu.com/p/507890541
