是否每個.NET中的集合類型都應該實現所有.NET類型接口?
英文原文: Should all .NET Collections Implement all .NET Collection Interfaces?
是否每個 .NET 中的集合類型都應該實現所有 .NET 類型接口?在 1 月 14 日進行的 .NET 核心 API 審查視頻中,這一問題在 API 相關的重要問題中居首位。這段視頻錄制了針對 .NET 基礎類庫的十個變更請求的相關討論。
[視頻] GitHub Issue:#316:為正則集合(包括 CaptureCollection、GroupCollection 和 MatchCollection)實現 IList<T>、IReadOnlyList<T>和 IList 接口
在 .NET 類庫中,但凡返回集合類型的屬性或方法,多數都會選擇使用強類型的集合。這種集合不是諸如 IEnumerable<Foo>或 IList<Foo>等類型,而是強類型的 FooCollection。這種方式對于向后兼容來說更為理想,因為可以放心地在類中加入新的方法,而不必依賴于不安全的類型轉換。
其實原因還不只是這一條。在 .NET 1.0 版本中還沒有出現泛型,意味著 IList<T>等接口也不存在,因此只能通過創建自定義的類型來保證類型安全。
當 .NET 發布之后,這些強類型中還有很大一部分沒有升級為支持泛型集合接口的類型。因此微軟所面對的第一個問題就是,是否應該為正則集合實現泛型接口?
第二個問題是,是否應該為只讀集合實現 IList<T>等接口?自從 .NET 2.0 發布以來,由于只讀集合接口的出現,IList<T>應當只用于可變集合類型,這一問題本應迎刃而解。但由于只讀集合接口相對較新,有許多 API 依然只支持 IList<T>類型,并僅僅通過文檔表示不會對其中的內容進行變更。如果某些方法需要對集合內容進行變更,理論上只需要檢查一下 IsReadOnly 屬性的值就可以了,但這完全取決于類庫開發者和應用程序開發者的自覺性。
再回到原來的問題上,只讀的正則集合是否應該支持 IList<T>接口,以便在讓不支持 IReadOnlyList<T>接口的遺留 API 也可以調用呢?還是應當選擇不支持 IList<T>接口,以減少被某些需要可變的列表的方法所誤用呢?
另一個對此問題起到影響的因素是,Windows Forms 中的數據綁定是依賴于 IList 接口的,因此如果要在用戶界面中使用這些正則類型,就必須添加該接口。
結論:按照當下的設計指南的做法,為這些類型實現所有接口。從長期的考慮來看,考慮對設計指南做某些調整。
與之相關的另一個問題是,是否應該讓這些類型繼承于 ReadOnlyCollection<T>類?雖然這種方式能夠免去大量的模板代碼,但會造成一些向后兼容的問題:
- 重載的解析會產生變化(這個問題同樣會發生在添加新接口的方案中)。
- 使用反射的結果也會產生變化。
結論:今后,所有新的只讀集合都將繼承于 ReadOnlyCollection<T>,而現有的類型則保持不變。
序列化是另一個問題。由于序列化類庫中的一些歷史悠久的 bug 的問題,如果為類型加入對 IList 或其它接口的支持,有可能造成原本正常的序列化失敗。
結論:如果造成了序列化的失敗,則必須撤消該變更。
[視頻] GitHub Issue:#110:為 XLinq 的 doucment 和 element 加載加入 async 實現
對于這一請求的第一個問題是,應該使用方法重載還是可選參數?對于這個 API 來說,問題主要是針對 cancellation token 參數而言,但同樣的問題也多次出現在其它場合中。
對于可選參數的反對意見在于它們對版本化的支持不理解。使用了可選參數之后,如果為某類型加入了一個接受更多參數的新重載,破壞重載解析的可能性就更大。
結論:可選參數值得一方式,但需要更多的指導與代碼分析,以確保它們適當地處理了版本化問題。
第二個問題是,是否要加入一個支持 URI 的 LoadAsync 方法。整個團隊對于該方法的同步版本頗有微詞,因為它為 XLinq 類庫引入了額外的依賴。
結論:如果能夠讓同步版本的 Load (string uri)方法過期,那么也不需要異步的方法了。否則的話,就需要創建一個對應的異步方法,這樣也可以便于將來過渡到 async 實現。
第三個問題是,是否應該為 Load 方法的所有重載都實現對應的 LoadAsync 方法?假設 cancellation token 參數依然保留的情況下,這種方法就將導致方法的數量從 4 個增加到 8 個。而如果 cancellation token 變成了可選參數,則 LoadAsync 的重載方法的數量將變成 16 個,其中的 12 個只是跳轉到另外 4 個主要的重載版本。
結論:首先考慮使用場合最多的方法簽名,今后再考慮加入方法重載或可選參數。
[視頻] GitHub Issue:#400:為 ImmutableArray<T>類型加入 Cast<T>和 CastFrom<TDerived>方法
對于不可變數組來說,它們可能會遇到的轉換有三種情景:
- 靜態轉換為某個基礎類型,這種轉換必然會成功。例如將 ImmutableArray<Dog>轉換為 ImmutableArray<Animal>
- 動態轉換,這種轉換有可能會失敗。例如將 ImmutableArray<Animal>轉換為 ImmutableArray<Dog>。
- 有條件地轉換為某個繼承類型,即大家所熟悉的“as 轉換”。
目前只有最后一種轉換方式已經得到了完整的支持。通過 ImmutableArray.Create 方法的某個重載可以支持靜態轉換,但要找到這個重載并不容易,而且這種方法是否會造成額外的內存分配不是一眼就能夠看出來的。
對此問題的一個建議是,刪除那個不直觀的 Create 方法。添加 Cast 和 CastFrom 方法,以實現靜態和動態轉換操作。
對這個 API 的第一個問題是,是否應該使用 Cast 這個方法名稱?主要問題在于,Cast 這個名稱與 LINQ 中用于元素延遲轉換的方法同名。最理想的方案是將 LINQ 中的方法名稱改為 CastElements<T>,但這一點明顯是不現實的。
另一個相關的問題是和 As 方法名有關的。通常來說,.NET 會使用 ToXxx 的方法名表示會造成內存分配的轉換,而用 AsXxx 的方法名表現無內存分配的轉換。而不可變數組中的 As 方法實際上是與 C# 中的“as”操作符具有相同的作用,而不是遵循傳統意義上的做法。
另一個問題是和 Visual Basic 有關的。與 C# 不同,在 VB 中可以通過某個實例變量調用它的靜態方法。雖然這種方式讓人感覺不爽,但它確實已經成為了 VB 語言中的一部分,因此類似 CastFrom 這樣的靜態方法就讓人搞不清到底是將什么轉換為什么。
結論:方法名稱依然有改善余地,除此之外,這個提議還是非常有用的。
[視頻] GitHub Issue:#394:為 ConcurrentDictionary<TKey, TValue> 類型加入 GetOrAdd 和 AddOrUpdate 方法重載,其中包括一個 TArg 參數 factoryArgument
接下來登場的是一個 pull request 中的內容,即為 ConcurrentDictionary 加入新的方法重載。要理解這幾個重載的目的,你首先必須理解閉包是如何工作的。當一個閉包產生時,必須對閉包中引用的變量分配內存。如果在一個數量巨大的 循環中使用閉包,就會導致大量的內存占用。
與之相反的是,如果某個匿名方法中沒有捕捉(capture)任何本地變量,那么指向該方法的委托就能夠被編譯器進行緩存并重用。這就使得對該方法的調用不會產生內存分配。
這些新的方法重載允許你為 GetOrAdd 和 GetOrUpdate 方法中所使用的工廠方法加入一個額外的值。一般情況下,這個額外的值足以代替閉包的使用,因此可以減少內存的占用。
這些方法也可以被實現為擴展方法,其性能和普通方法相比基本相同。因此對這個問題的討論主要在于,該方法帶來的性能優勢和作為普通方法的便利性,是否能夠抵消由此造成的類的體積膨脹的缺點。
一個更廣泛的問題是,這種模式是否應該在整個 .NET 平臺中使用。幾乎每個使用了委托的方法,都可以通過這一模式移除對閉包的使用。
結論:如果能夠證明新的方法對性能產生了很大的改善,就決定加入這些新方法。
明天我們還將繼續對這次 API 審查會議的分析。