通過抽象接口引入有限形式的多重繼承,這一.NET新提議頗具爭議性。該特性是受Java默認(rèn)方法(Default Methods)的啟發(fā)。
默認(rèn)方法的目的在于允許開發(fā)人員修改已發(fā)布的抽象接口。修改已發(fā)布接口將會產(chǎn)生破壞性的更改,因此在Java和.NET中通常是不允許的。默認(rèn)方法的提出,為接口編寫者提供了一種可重寫的實現(xiàn),緩解了向后兼容問題。
在C#版本的提議中,將包括用于如下部分的語法:
方法體(即“默認(rèn)”實現(xiàn)); 屬性訪問器體; 靜態(tài)方法和屬性; 私有方法和屬性(默認(rèn)訪問是公開的); 覆寫方法和屬性。這個提議并不允許接口具有域,因此形式上是一種有限的多重繼承,但避免了一些已在C++中發(fā)現(xiàn)的問題(盡管域可以使用ConditionalWeakTable和擴(kuò)展屬性模式模擬)。
用例:IEnumerble.Count
為IEnumerable
正如從上例中可以看到的,實現(xiàn)IList
大家所關(guān)注的一個問題是該提議會使接口膨脹。既然可以在IEnumerable中添加Count方法,為什么不能添加其他所有的IEnumerable擴(kuò)展方法?
Eirenarch這樣寫道:
有人會認(rèn)真考慮將Count()添加到IEnumerable,對此我有點吃驚。這不是和Reset方法同樣的問題嗎?并非所有的IEnumerable都可重置,或是可安全地做計數(shù),因為其中的一些接口是一次性的。現(xiàn)在看這個問題,我想不起曾在IEnumerable上使用過Count(),只是在數(shù)據(jù)庫LINQ調(diào)用中使用過,因為我不想冒險讓Count()消費(fèi)可枚舉類型,或是變得低效。為什么要鼓勵更多的Count()?
DavidArno補(bǔ)充道:
哈哈,很高興能看到對這一提議的爭論。基礎(chǔ)類庫(BCL,Base Class Library)團(tuán)隊早就將各種集合類搞得混雜不堪。就這一點而言,我懷疑團(tuán)隊中是否有人真正考慮過Barbara Liskov的建議,她所提出的替換原則如此完全地被打破了。如果將這一提議中的理念賦予團(tuán)隊,這將允許他們造成更大的破壞。想想就十分可怕!
在一次BCL會議上:
“OK,各位,我們想讓IEnumerable
“這很簡單。默認(rèn)接口方法就能為我們解決這個問題。只需加入(T head, IEnumerable
“非常好,搞定。謝謝大家,本周會議結(jié)束。”
注意,LINQ是由另一個獨立團(tuán)隊負(fù)責(zé)的。LINQ的功能并沒有計劃要切實地遷移到IEnumerable中。
這一更改也會打破當(dāng)前擴(kuò)展方法所提供的層次。目前Enumerable.Count方法位于System.Core動態(tài)庫中,比mscorlib動態(tài)庫要高兩層。可能有人認(rèn)為將LINQ的部分或完全地加入mscorlib中,會造成該動態(tài)庫沒有必要的膨脹。
另一個批評意見認(rèn)為這一提議是沒有必要的,因為已經(jīng)存在允許可選地覆寫擴(kuò)展方法的設(shè)計模式。
可覆寫擴(kuò)展方法模式
可重新擴(kuò)展方法依賴于接口檢查。理想情況下只需要對一個接口做檢查。但是由于一些歷史遺留問題,以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
默認(rèn)方法,類的公有API
為了在添加新方法時,為避免向后兼容性問題,不能通過類的公有接口訪問默認(rèn)方法。以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上暴露接口方法,需要添加樣板代碼。這樣的做法限制了對類提供默認(rèn)實現(xiàn)這一技術(shù)的有用性。
使用一個默認(rèn)方法覆寫另一個默認(rèn)方法
一個接口中的默認(rèn)方法可以覆寫另一個接口中的默認(rèn)方法。這一點可以在IEnumerable.Count用例中看到。
正常情況下,需要對方法顯式地指定override關(guān)鍵字,否則新方法在處理上將與其它方法無關(guān)。
也可以將一個接口方法標(biāo)記為“override abstract”。通常沒有必要指定abstract關(guān)鍵字,因為所有的抽象接口方法默認(rèn)就是abstract的。
擴(kuò)展方法與默認(rèn)參數(shù)的解析順序
Zippec提出了一個重要問題,即如果新添加的接口方法與用于該接口的擴(kuò)展方法命名相同時,將會發(fā)生什么:
將當(dāng)前API升級為默認(rèn)方法時會發(fā)生什么?我是否可以認(rèn)為它們應(yīng)該比擴(kuò)展方法在覆寫解析上具有更高的優(yōu)先級?讓我們以Count()方法為例。我們可以從IEnumerable上得到該方法嗎?如果是這樣,它將隱藏使用該特性在C#中重新編譯后的LINQ IEnumerable.Count()實現(xiàn),這是否會更改被調(diào)用的代碼?我認(rèn)為對于IQueryable,這是一個問題。
如果該問題存在,為緩解該問題,我們在BCL中以屬性方式得到Count方法。由于默認(rèn)方法會更改任何自定義擴(kuò)展方法的現(xiàn)有實現(xiàn),這是否意味著在已經(jīng)存在的BCL接口上,我們永遠(yuǎn)無法獲得任何默認(rèn)方法(只能獲得屬性)?
盡管不常見,一些開發(fā)人員的確創(chuàng)建了自己的擴(kuò)展方法庫,鏡像了LINQ中的庫,但是具有不同的行為。如果擴(kuò)展方法是作為默認(rèn)方法移入接口中的,那么就會失去置換擴(kuò)展庫的能力。
用例: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方法,沒有定義用于存儲事件句柄列表的底層代理。
在提議中并未考慮這一問題,因此該問題是可以更改的。通用語言運(yùn)行平臺(CLR)的確為存儲事件的“生成Accessor方法”預(yù)留了位置,雖然當(dāng)前僅能使用VB語言。
更多支持的聲音
HaloFour寫道:
這看上去非常像是一個意識形態(tài)上的爭論。其中有一些已知的問題自發(fā)布.NET 1.0以來,就一直沒有被團(tuán)隊很好的解決。長期以來,標(biāo)準(zhǔn)解決方案一直是擺在那里的,但是這些方案常將API弄得完全一團(tuán)糟,給出了IFoo、IFoo2、IFoo3、IFooEx、IFooSpecial、IFooWithBar這樣的內(nèi)容。擴(kuò)展方法為解決這些問題做了大量工作,但是局限于那些可以在擴(kuò)展方法中明確識別和分發(fā)的問題。除此之外,擴(kuò)展方法缺乏特化(Specialization)。
默認(rèn)實現(xiàn)很好地解決了這些問題。它允許Java團(tuán)隊使用額外的幫助方法(Helper Method)覆寫在Java中已長期存在的接口,其中一些的確通過各種實現(xiàn)得以特化,例如Map#computeIfPresent。
其它一些批評的聲音
HerpDerpImARedditor寫道:
噢,該提議會引發(fā)那些積習(xí)難改的面條式代碼(Spaghetti Code)。可能我考慮不周全,敬請諒解,但是這個模式解決了哪些在實現(xiàn)層無法解決的問題?這樣的提議看上去抹煞了接口與具體實現(xiàn)之間存在的華麗差異。是否要讓IDE完全指定運(yùn)行時的出處?我不能認(rèn)為這能與控制反轉(zhuǎn)(IoC)一起工作。
當(dāng)然,我十分熱愛.NET,我歷經(jīng)了從經(jīng)典的ASP/VB開發(fā)背景直至.NET 1。這是第一個我所反對的語言規(guī)格添加(雖然當(dāng)“dynamic”登場亮相時,我在看到它的用例后的確在立場上做了一些讓步)。雖然我看見一些人說他們將會忽略存在這一特性,但是我關(guān)注的是,可能今后我會在其他人的代碼中碰上這樣的特性,所以不能無視它。
當(dāng)然還好,我猜測在任何真正的判決被通過前,我們不會看到這樣的特性起作用。
Canthros寫道:
這讓我很沮喪。
從Github上的討論看,LINQ的各種擴(kuò)展方法所實現(xiàn)的一團(tuán)糟引發(fā)了一些不滿,尤其是為提供優(yōu)化實現(xiàn)所必需完成的類型檢測(type-sniffing)。雖然這一特性概化到語言特性中可能會大大降低.NET Core實現(xiàn)者的工作,但付出的代價是語言需要承受接口和抽象類之間區(qū)分混亂,并且特性中大量吸入了下游存在的問題。
看起來Shapes提議可以大大緩解這個問題,但是現(xiàn)在我無暇切實考慮全部的問題。
查看英文原文: .NET Futures: Multiple Inheritance