【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】
Dart 宣告點變異數
宣告點變異數是我的 Dart 團隊實習專案,我在 Dart 實習生的生活 這篇文章中記錄了我在團隊中的個人經驗。作為 宣告點變異數功能 的主要實作者,我想分享可靠變異數的用法和好處。
我們將討論如何使用變異數、為什麼我們想要使用修飾符、該功能如何建構在不使用修飾符的類別之上,以及此功能為我們提供了哪些好處。
注意: 變異數的實作尚未完成。雖然您可以透過啟用實驗(說明如下)來試用它,但在最終確定之前,該功能可能會發生變化。
在深入探討 Dart 的宣告點變異數功能之前,我們將快速繞道討論變異數的含義以及它的使用方法。
什麼是變異數?
為了簡要介紹變異數,我們可以看看這個例子:
1 | Iterable<int> integers = [1, 2, 3]; |
由此可見,整數的 Iterable 可以替換為物件的 Iterable,因為整數是一個物件,並且可以在 Iterable 中的任何物件使用的地方使用。該語言允許這樣做,方法是說如果兩個相同泛型類型(例如這裡的 Iterable<int>
和 Iterable<Object>
)的類型引數(int
和 Object
)是子類型,則它們被視為子類型。這種子類型關係被視為協變。
這很方便且合乎邏輯。這很有道理,也就是說,直到您查看方法參數的變異數。假設您想在 Dart 中建立一個 objectWriter
:
1 | class Writer<T> { |
然後,您急切地讓您的 objectWriter
寫入一個字串,卻發現它產生了執行時錯誤。編譯器允許這段程式碼,但是當您執行它時,它會拋出一個異常。
1 | type 'Writer<int>' is not a subtype of type 'Writer<Object>' in type cast |
這是為什麼呢?對於逆變,子類型關係與協變相反。我們需要能夠向 objectWriter
寫入任何物件,但是從前面我們知道 objectWriter
實際上是一個偽裝的整數寫入器。
您需要知道的最後一個變異數類型是不變。不變的子類型關係意味著兩個不變的類型之間沒有子類型關係,除非它們是完全相同的類型。
1 | class Invariant<T> { |
Dart 中的變異數功能是什麼?
由於 Dart 團隊提議在語言中加入明確的變異數修飾符,我們將預覽一些預期的變化。
Dart 將具有可以應用於類別和混入中的類型參數的變異數修飾符。語法類似於 C# 中的變異數修飾符。
您可以分別使用關鍵字 out
、in
和 inout
來宣告協變、逆變和不變的類型參數。這與泛型類型一起使用,如下所示:
1 | class Covariant<out T> {} |
為什麼要為泛型類型定義明確的變異數?為什麼我們想要這個功能?
Dart 的靜態類型系統目前將所有類型參數視為協變。這對於泛型來說是正確且方便的,其中類型在安全協變的位置(例如返回類型)中使用。但是,當類型引數應該是逆變或不變時,這是錯誤的:
1 | class Writer<T> { |
當您使用 objectWriter
時,您期望能夠寫入任何物件。不幸的是,objectWriter
只寫入整數。編譯器不知道任何更好的方法,當您執行程式碼時,您會收到可怕的執行時錯誤。為了避免不健全,如果您以不安全的方式使用類型引數,Dart 會在執行時拋出錯誤。
幸運的是,加入變異數修飾符會將這種不正確的使用從執行時錯誤轉變為編譯時錯誤。
1 | class Writer<in T> { // 加入 'in' 修飾符 |
這樣好多了。早在您寫入 objectWriter.write("我是一個字串!");
之前,編譯器就會通知您有問題。
現在,讓我們來看看使用變異數修飾符加入安全類型的參數會為您提供什麼。
成員中的類型參數
如果您使用 out
標記泛型類型參數,則如果您在方法或欄位中在不安全協變的位置(例如返回類型)中使用該類型,編譯器會發出靜態錯誤。同樣地,標記為 in
的類型參數只能在安全逆變的位置(例如方法參數類型)中使用。標記為 inout
的類型參數可以在任何地方使用。
以下是一些您可能會覺得有用的方法變異數位置錯誤和正確用法。相同的錯誤檢查也發生在混入中。
1 | class Covariant<out T> { |
欄位中也可能會發出錯誤。
1 | class Covariant<out T> { |
分配和子類型
編譯器報告的關於類型參數誤用的錯誤可以幫助泛型類別作者編寫正確的程式碼。另一半是幫助其他人正確使用類別的一組錯誤。可靠變異數修飾符帶來的變化之一是我們可以透過分配看到的子類型變化。
如果泛型類型參數是協變的,那麼當其類型引數是預期類型的類型引數的子類型時,您可以分配它。例如,您可以將 Reader<int>
分配給預期 Reader<Object>
的內容。
1 | class Reader<out T> {} |
同樣地,如果泛型類型參數是逆變的,則當其類型引數是預期類型的類型引數的超類型時,允許分配。您可以將 Writer<Object>
分配給預期 Writer<int>
的內容,如下所示:
1 | class Writer<in T> {} |
對於不變的參數,類型引數必須是相同的類型。
1 | class Invariant<inout T> {} |
介面繼承
所以您可能會問:「即使繼承舊的類別,我們也可以選擇使用變異數進行更強的編譯時檢查嗎?」好消息是您可以;但是,有一些限制。
out
參數只能繼承協變或具有預設 Dart 類型變異數的參數位置。
請記住,從傳統類別繼承的任何方法仍然可能是變異數不健全的,因此仍然可能導致執行時錯誤。否則,如果類型在不健全的位置,具有變異數修飾符的類型參數的子類別中的所有新方法都將發出錯誤。
1 | // 傳統類別 |
in
參數只能繼承逆變的參數位置。
1 | class Contravariant<in T> { |
inout
參數可以繼承所有參數位置。但是,定義為 inout
的參數只能由其他不變位置繼承。
1 | class Invariant<inout T> { |
如何提供關於變異數功能的回饋?
我們建議使用 最新的開發頻道 試用變異數功能。試用這個例子,以掌握變異數的工作原理以及它可以為您做些什麼。
1 | void main() { |
因為變異數功能仍在實作中,您需要設定一個實驗性標誌來啟用它:
1 | dart --enable-experiment=variance variance_example.dart |
我們感謝任何和所有回饋!您可以在 GitHub 問題 中告訴我們您的想法。
總結
現有的 Dart 泛型預設為協變,這使得開始編寫新類別和入門變得容易。然而,這意味著更多錯誤出現在執行時而不是編譯時。使用者還要付出額外的執行時檢查的成本。變異數背後的主要思想是在編譯時為使用者提供更多資訊的錯誤檢查。
變異數僅針對泛型類別和混入的參數定義。使用者可以透過在類型參數前面加入 in
、out
或 inout
關鍵字之一來使用變異數功能。
此外,具有這些修飾符的新泛型介面可以繼承沒有變異數修飾符的傳統介面。
宣告點變異數允許您獲得許多新的好處,包括:
- 介面成員內的編譯時變異數位置檢查
- 移除由向下和向上轉換引起的煩人的執行時錯誤
- 根據宣告的變異數進行額外的子類型更改
- 更具資訊性且易於存取的錯誤檢查
現在,您不必擔心 objectWriter
是否真的是任何物件的寫入器。您知道它是。
Dart 宣告點變異數 最初發佈在 Dart 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。