未來的.NET之多重繼承

責任編輯:editor004

作者:Jonathan Allen

2017-04-19 11:23:56

摘自:INFOQ

通過抽象接口引入有限形式的多重繼承,這一 NET新提議頗具爭議性。另一個批評意見認為這一提議是沒有必要的,因為已經(jīng)存在允許可選地覆寫擴展方法的設計模式

通過抽象接口引入有限形式的多重繼承,這一.NET新提議頗具爭議性。該特性是受Java默認方法(Default Methods)的啟發(fā)。

默認方法的目的在于允許開發(fā)人員修改已發(fā)布的抽象接口。修改已發(fā)布接口將會產(chǎn)生破壞性的更改,因此在Java和.NET中通常是不允許的。默認方法的提出,為接口編寫者提供了一種可重寫的實現(xiàn),緩解了向后兼容問題。

在C#版本的提議中,將包括用于如下部分的語法:

方法體(即“默認”實現(xiàn)); 屬性訪問器體; 靜態(tài)方法和屬性; 私有方法和屬性(默認訪問是公開的); 覆寫方法和屬性。

這個提議并不允許接口具有域,因此形式上是一種有限的多重繼承,但避免了一些已在C++中發(fā)現(xiàn)的問題(盡管域可以使用ConditionalWeakTable和擴展屬性模式模擬)。

用例:IEnumerble.Count

為IEnumerable添加Count方法是該特性最廣為使用的用例。具體做法并非使用Enumerable.Count這一擴展方法,而是開發(fā)人員可以免費獲取Count方法,并且如果開發(fā)人員能夠提供更高效實現(xiàn)的話,能夠可選地(optionally)覆寫該方法:

interface IEnumerable{ int Count() { int count = 0; foreach (var x in this) count++; return count; }}interface IList ...{ int Count { get; } override int IEnumerable.Count() => this.Count;}

正如從上例中可以看到的,實現(xiàn)IList的開發(fā)人員無需擔心覆寫IEnumerable.Count()方法,因為它將自動獲得IList.Count。

大家所關(guān)注的一個問題是該提議會使接口膨脹。既然可以在IEnumerable中添加Count方法,為什么不能添加其他所有的IEnumerable擴展方法?

Eirenarch這樣寫道:

有人會認真考慮將Count()添加到IEnumerable,對此我有點吃驚。這不是和Reset方法同樣的問題嗎?并非所有的IEnumerable都可重置,或是可安全地做計數(shù),因為其中的一些接口是一次性的?,F(xiàn)在看這個問題,我想不起曾在IEnumerable上使用過Count(),只是在數(shù)據(jù)庫LINQ調(diào)用中使用過,因為我不想冒險讓Count()消費可枚舉類型,或是變得低效。為什么要鼓勵更多的Count()?

DavidArno補充道:

哈哈,很高興能看到對這一提議的爭論?;A類庫(BCL,Base Class Library)團隊早就將各種集合類搞得混雜不堪。就這一點而言,我懷疑團隊中是否有人真正考慮過Barbara Liskov的建議,她所提出的替換原則如此完全地被打破了。如果將這一提議中的理念賦予團隊,這將允許他們造成更大的破壞。想想就十分可怕!

在一次BCL會議上:

“OK,各位,我們想讓IEnumerable接口支持cons功能。大家有何建議?”

“這很簡單。默認接口方法就能為我們解決這個問題。只需加入(T head, IEnumerable tail) Cons() => throw new NotImplementedException(),這就完事了。IEnumerable的實現(xiàn)者完全可以在閑暇時添加這一實現(xiàn)。”

“非常好,搞定。謝謝大家,本周會議結(jié)束。”

注意,LINQ是由另一個獨立團隊負責的。LINQ的功能并沒有計劃要切實地遷移到IEnumerable中。

這一更改也會打破當前擴展方法所提供的層次。目前Enumerable.Count方法位于System.Core動態(tài)庫中,比mscorlib動態(tài)庫要高兩層??赡苡腥苏J為將LINQ的部分或完全地加入mscorlib中,會造成該動態(tài)庫沒有必要的膨脹。

另一個批評意見認為這一提議是沒有必要的,因為已經(jīng)存在允許可選地覆寫擴展方法的設計模式。

可覆寫擴展方法模式

可重新擴展方法依賴于接口檢查。理想情況下只需要對一個接口做檢查。但是由于一些歷史遺留問題,以Enumerable.Count為例,需要檢查兩個接口。代碼如下:

public static int Count(this IEnumerable source) { var collectionoft = source as ICollection; if (collectionoft != null) return collectionoft.Count; var collection = source as ICollection; if (collection != null) return collection.Count; int count = 0; using (var e = source.GetEnumerator()) { while (e.MoveNext()) count++; } return count;}

(為清楚起見,例子中移除了錯誤處理的代碼。)

這個模式的缺點是存在可選接口過于寬泛的問題。例如,如果在類中想要覆寫Enumerable.Count方法,那么需要實現(xiàn)整個ICollection接口。對于只讀類,則要編寫大量的NotSupported異常(重申一下,這里由于歷史原因要查看的是ICollection接口,而非更小的IReadOnlyCollection接口)。

默認方法,類的公有API

為了在添加新方法時,為避免向后兼容性問題,不能通過類的公有接口訪問默認方法。以IEnumerable.Count為例,看下面的類:

class Countable : IEnumerable{ public IEnumerator GetEnumerator() {…}}

鑒于并未覆寫IEnumerable.Count方法,因此不能這樣編寫代碼:

var x = new Countable();var y = x.Count();

而是需要做類型轉(zhuǎn)換:

var y = ((IEnumerable)x).Count();

這時為實現(xiàn)在類的公有API上暴露接口方法,需要添加樣板代碼。這樣的做法限制了對類提供默認實現(xiàn)這一技術(shù)的有用性。

使用一個默認方法覆寫另一個默認方法

一個接口中的默認方法可以覆寫另一個接口中的默認方法。這一點可以在IEnumerable.Count用例中看到。

正常情況下,需要對方法顯式地指定override關(guān)鍵字,否則新方法在處理上將與其它方法無關(guān)。

也可以將一個接口方法標記為“override abstract”。通常沒有必要指定abstract關(guān)鍵字,因為所有的抽象接口方法默認就是abstract的。

擴展方法與默認參數(shù)的解析順序

Zippec提出了一個重要問題,即如果新添加的接口方法與用于該接口的擴展方法命名相同時,將會發(fā)生什么:

將當前API升級為默認方法時會發(fā)生什么?我是否可以認為它們應該比擴展方法在覆寫解析上具有更高的優(yōu)先級?讓我們以Count()方法為例。我們可以從IEnumerable上得到該方法嗎?如果是這樣,它將隱藏使用該特性在C#中重新編譯后的LINQ IEnumerable.Count()實現(xiàn),這是否會更改被調(diào)用的代碼?我認為對于IQueryable,這是一個問題。

如果該問題存在,為緩解該問題,我們在BCL中以屬性方式得到Count方法。由于默認方法會更改任何自定義擴展方法的現(xiàn)有實現(xiàn),這是否意味著在已經(jīng)存在的BCL接口上,我們永遠無法獲得任何默認方法(只能獲得屬性)?

盡管不常見,一些開發(fā)人員的確創(chuàng)建了自己的擴展方法庫,鏡像了LINQ中的庫,但是具有不同的行為。如果擴展方法是作為默認方法移入接口中的,那么就會失去置換擴展庫的能力。

用例:INotifyPropertyChanged

下面給出了另一個用例,一般人們在考慮新特性時可能使用:

interface INotifyPropertyChanged{ event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(args); protected void OnPropertyChanged([CallerMemberName] string propertyName) => OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); protected void SetProperty(ref T storage, T value, PropertyChangedEventArgs args) { if (!EqualityComparer.Default.Equals(storage, value)) { storage = value; OnPropertyChanged(args); } } protected void SetProperty(ref T storage, T value, [CallerMemberName] string propertyName) => SetProperty(ref storage, value, new PropertyChangedEventArgs(propertyName));}

但是,該用例并不會真正工作,因為接口沒有提供生成事件的方法。接口只是定義了事件的Add和Remove方法,沒有定義用于存儲事件句柄列表的底層代理。

在提議中并未考慮這一問題,因此該問題是可以更改的。通用語言運行平臺(CLR)的確為存儲事件的“生成Accessor方法”預留了位置,雖然當前僅能使用VB語言。

更多支持的聲音

HaloFour寫道:

這看上去非常像是一個意識形態(tài)上的爭論。其中有一些已知的問題自發(fā)布.NET 1.0以來,就一直沒有被團隊很好的解決。長期以來,標準解決方案一直是擺在那里的,但是這些方案常將API弄得完全一團糟,給出了IFoo、IFoo2、IFoo3、IFooEx、IFooSpecial、IFooWithBar這樣的內(nèi)容。擴展方法為解決這些問題做了大量工作,但是局限于那些可以在擴展方法中明確識別和分發(fā)的問題。除此之外,擴展方法缺乏特化(Specialization)。

默認實現(xiàn)很好地解決了這些問題。它允許Java團隊使用額外的幫助方法(Helper Method)覆寫在Java中已長期存在的接口,其中一些的確通過各種實現(xiàn)得以特化,例如Map#computeIfPresent。

其它一些批評的聲音

HerpDerpImARedditor寫道:

噢,該提議會引發(fā)那些積習難改的面條式代碼(Spaghetti Code)。可能我考慮不周全,敬請諒解,但是這個模式解決了哪些在實現(xiàn)層無法解決的問題?這樣的提議看上去抹煞了接口與具體實現(xiàn)之間存在的華麗差異。是否要讓IDE完全指定運行時的出處?我不能認為這能與控制反轉(zhuǎn)(IoC)一起工作。

當然,我十分熱愛.NET,我歷經(jīng)了從經(jīng)典的ASP/VB開發(fā)背景直至.NET 1。這是第一個我所反對的語言規(guī)格添加(雖然當“dynamic”登場亮相時,我在看到它的用例后的確在立場上做了一些讓步)。雖然我看見一些人說他們將會忽略存在這一特性,但是我關(guān)注的是,可能今后我會在其他人的代碼中碰上這樣的特性,所以不能無視它。

當然還好,我猜測在任何真正的判決被通過前,我們不會看到這樣的特性起作用。

Canthros寫道:

這讓我很沮喪。

從Github上的討論看,LINQ的各種擴展方法所實現(xiàn)的一團糟引發(fā)了一些不滿,尤其是為提供優(yōu)化實現(xiàn)所必需完成的類型檢測(type-sniffing)。雖然這一特性概化到語言特性中可能會大大降低.NET Core實現(xiàn)者的工作,但付出的代價是語言需要承受接口和抽象類之間區(qū)分混亂,并且特性中大量吸入了下游存在的問題。

看起來Shapes提議可以大大緩解這個問題,但是現(xiàn)在我無暇切實考慮全部的問題。

查看英文原文: .NET Futures: Multiple Inheritance

鏈接已復制,快去分享吧

企業(yè)網(wǎng)版權(quán)所有?2010-2024 京ICP備09108050號-6京公網(wǎng)安備 11010502049343號