.NET中只讀集合接口的故事

jopen 13年前發布 | 10K 次閱讀 .NET

.NET 4.5 中添加了兩個新的集合接口,IReadOnlyList 和 IReadOnlyDictionary。盡管這些接口表面上看起來是如此稀松平常,但是他們卻揭露了與向后兼容性、互操作性、以及協變的作用等有關的相當復雜的故事。

IReadOnlyList 和 IReadOnlyDictionary 是 .NET 開發者自始至終都想得到的接口。只讀接口除了提供某種對稱性之外,還應消除那些什么都不做而只拋出 NotSupportedException 異常的方法。由于某些隨時間流逝已不可知的原因,此接口并未完成。

接下來一次機會是在 .NET 2.0 中引入泛型。這使得微軟可以淘汰弱類型集合,并使用強類型變體替代它們。基類庫[1]團隊再次錯過了這個提供只讀列表(read-only list)的機會,正如 Kit George 所寫的那樣

因為對于你與 Joe 所談論的問題,我們打算提供一種缺省實現,而不是給你一個接口,所以我們提供了 ReadOnlyCollectionBase 基類。然而,鑒于它不是強類型的,我能理解人們不愿使用它的原因。但隨著泛型的引入,我們現在同時擁有了 ReadOnlyCollection<T>,所以你不僅獲得了同等的功能,而且就是強類型的:太棒了!

ReadOnlyCollection<T>不是密封類,因此如果需要可以隨意在此之上編寫你自己的集合。自從我們為此創作的這些集合可適合一般需求以來,我們尚沒有計劃為與此相同的概念引入接口。

Krzysztof Cwalina 也對此主題進行了評論,

無論這聽起來令人驚訝與否,但是 IList 和 IList<T>是我們所期望的只讀集合接口。它們都擁有 IsReadOnly 布爾型屬性,當某個只讀集合實現此屬性后應返回 true。我們不想添加純粹的只讀接口的原因是,我們覺得它會給基類庫添加太多不必要的復雜性。請注意,就復雜性而言,我們既指此新接口又指其消費者。

我們覺得 API 的設計者們要么是并不關心在運行時檢查 IsReadOnly 屬性、及其可能拋出的異常,在這種情況下使用 IList 接口就不錯,要么他們愿意提供一個真正整潔的自定義 API,在這種情況下他們應顯示實現 IList 接口、并公布自定義的簡潔的只讀 API。對于從對象模型中公開的集合而言,后者是種典型方式。

盡管開發曾抱怨此種情況,由于泛型所提供的新機會遠遠大于這個癥結,因此該問題在 .NET 4 以前很大程度上被忽視了。然而,此決定也引發了一些反響,我們將在稍后討論。

隨著在 .NET 4 中一個令人興奮的新功能被添加到運行時。當早期版本的 .NET 接口出現在類型中時,那些接口是被過度限制的。例如,即使 Customer 繼承自 Person,也無法將類型為 IEnumerable<Customer>的對象作為參數類型為 IEnumerable<Person>的函數的參數使用。隨著協變支持的添加,該限制才得以部分解除。

我們之所以說“部分”,是因為在某些情景下,相對于 IEnumerable 接口而言,人們更愿意使用一個具有豐富 API 的接口。而且當 IList 接口不支持協變的時候,至少該有一個只讀列表接口。不幸的是,.NET 基類庫團隊再次決定不解決這個疏忽。

接著,WinRT 的引入和 COM 的死灰復燃改變了一切。COM 互操作性曾是開發者在別無選擇的情況下才使用的一種技術,但現已成為 .NET 編程的基石。而且由于 WinRT 公開了 IVectorView<T>IMapView<K, V>接口,因此 .NET 必須與時俱進。

WinRT 計劃中一個頗為有趣的功能是,為每個開發平臺公布不同但功能類似的 API。正如你可能已經知道的,通過 JavaScript 開發者的眼睛所看到的是,所有方法名都是駝峰式大小寫(camelCased[2])表示的,而 C++ 和 .NET 開發者所看到方法則是以帕斯卡大小寫(PascalCased[3])表示的。另一處更加劇烈的變化是,在 C++ 與 .NET 的接口之間實現自動映射。因此 .NET 開發者無需處理 Windows.Foundation.Collections 命名空間,而是繼續使用 System.Collections.Generic 命名空間。IVectorView<T>和 IMapView<K, V>這兩個接口會被運行庫轉化為 IReadOnlyList<T>IReadOnlyDictionary<TKey, TValue>

值得注意的是,在C++/WinRT 中的這些接口名在某定程度上是更準確的。這些接口是用來表示針對某集合的一些視圖,但是接口并不確保該集合本身是不可變的。即使在那些經驗豐富的 .NET 開發者中也很常見的一種錯誤是,假設 ReadOnlyCollection 類型的對象是某個集合的不可變副本,其實,事實上此對象僅僅是對某活動集合的包裝(wrapper)(關于只讀、凍結、且不可變集合的詳細信息,請參閱 Andrew Arnott 的同名帖子)。

當得知盡管 IList<T>接口具有與 IReadOnlyList<T>接口所有相同的成員、并且所有列表都可表示為只讀列表,而 IList<T>卻不是繼承自 IReadOnlyList<T>以后,有人可能會覺得很有趣。Immo Landwerth 解釋說

這看起來是個合理的假設,它之所以能工作是因為那些只讀接口是可讀寫接口的純粹子集。不幸的是,此假設與預期不符,因為在元數據級別上位于每個接口上的每個方法都有其自己的槽(這使得顯式接口實現得以工作)。

或者換言之,他們必須將只讀接口作為那些可變種類的基類引入的唯一機會就是退回到 .NET 2.0,即它們最初被構思出來的時候。一旦放虎歸山,對其能做的唯一改變就是添加協變和/或逆變標記(在 VB 和 C# 中表示為“in”和“out”)。

當被問及為什么沒有 IReadOnlyCollection<T>接口時,Immo 回答說,

我們曾考慮過這個設計,但是我們覺得加入一個提供僅有 Count 屬性的類型對于基類庫而言不會增加很多價值。在基類庫團隊中,我們認為,如果一個 API 從負1000點開始,那么即使能提供一些價值也不足以證明可被添加。添加新 API 的理由也包括成本,例如,開發者會擁有更多可供選擇的概念。起初我們認為,添加這個類型將使得代碼在某些場景(你只想獲得計數,然后對它做一些有趣的東西)下獲得更好的性能。例如,批量添加到現有集合。然而,在這些場景下,我們鼓勵人們僅采用一個 IEnumerable<T>接口,而且對于擁有實現了 ICollection<T>接口的實例的特殊情況也是如此。自從所有我們的內建集合類型實現了此接口之后,然而在那些最常見的情況下并未沒有獲得任何性能收益。順便說一下,針對 IEnumerable<T>的擴展方法 Count ()同樣可以完成此功能。

這些新接口可用于 .NET 4.5 和 .NET for Windows 8。

譯注

[1] 基類庫,Base Class Library,縮寫為 BCL。有關基類庫的更多信息,請參與 MSDN

[2] camelCased,駝峰式命名法,又稱小駝峰式命名法(lower camel case)。格式為,第一個單字以小寫字母開始;第二個單字的首字母大寫,例如:firstName、lastName。

[3] PascalCased,帕斯卡命名法,又稱大駝峰式命名法(upper camel case)。格式為,每一個單字的首字母都采用大寫字母,例如:FirstName、LastName、CamelCase。

查看英文原文:The Story of Read-Only Collection Interfaces in .NET
      來自: InfoQ

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!