面向 API 維護者的類修飾符
Dart 3.0 添加了一些新的修飾符,你可以將它們放在類和mixin 宣告上。如果你是庫包的作者,這些修飾符可以讓你更好地控制使用者被允許如何使用你的包匯出的型別。這可以使你的包更容易演進,並且更容易知道程式碼的更改是否會破壞使用者。
Dart 3.0 還包括一項關於將類用作 mixin 的破壞性變更。此變更可能不會破壞*你的*類,但可能會破壞你類的*使用者*。
本指南將引導你瞭解這些變更,以便你知道如何使用新修飾符,以及它們如何影響你的庫使用者。
類上的 mixin 修飾符
#最重要的修飾符是 mixin。Dart 3.0 之前的語言版本允許任何類在另一個類的 with 子句中用作 mixin,*除非*該類:
- 聲明瞭任何非工廠建構函式。
- 擴充套件了
Object以外的任何類。
這使得你很容易在不瞭解其他使用者在 with 子句中使用類的情況下,透過向類新增建構函式或 extends 子句來意外地破壞他人的程式碼。
Dart 3.0 不再預設允許類用作 mixin。相反,你必須透過宣告一個 mixin class 來明確選擇啟用該行為。
mixin class Both {}
class UseAsMixin with Both {}
class UseAsSuperclass extends Both {}如果你將包更新到 Dart 3.0 並且不更改任何程式碼,你可能看不到任何錯誤。但如果你的包使用者將你的類用作 mixin,你可能會無意中破壞他們。
將類遷移為 mixin
#如果類有非工廠建構函式、extends 子句或 with 子句,那麼它已經不能用作 mixin。Dart 3.0 的行為不會改變;你無需擔心,也無需做任何事情。
實際上,這描述了大約 90% 的現有類。對於其餘可用作 mixin 的類,你必須決定要支援什麼。
以下是一些有助於決定的問題。第一個是務實的:
- 你是否想冒破壞任何使用者的風險?如果答案是肯定的“不”,那麼在可以作為 mixin 使用的任何類之前放置
mixin。這完全保留了 API 的現有行為。
另一方面,如果你想借此機會重新思考 API 所提供的能力,那麼你可能*不*想將其變為 mixin class。考慮以下兩個設計問題:
你是否希望使用者能夠直接構造其例項?換句話說,該類是否特意不是抽象的?
你*希望*人們能夠將此宣告用作 mixin 嗎?換句話說,你希望他們能夠在
with子句中使用它嗎?
如果兩者都回答“是”,則將其設為 mixin 類。如果第二個回答“否”,則將其保留為類。如果第一個回答“否”且第二個回答“是”,則將其從類更改為 mixin 宣告。
最後兩個選項,將其保留為類或將其轉換為純粹的 mixin,都是破壞性 API 變更。如果你這樣做,你需要提升你包的主版本號。
其他可選修飾符
#將類作為 mixin 處理是 Dart 3.0 中影響包 API 的唯一關鍵變更。一旦你完成了這一步,如果你不想對包允許使用者執行的操作進行其他更改,你就可以停止了。
請注意,如果你繼續並使用下面描述的任何修飾符,這可能是對你的包 API 的破壞性更改,需要增加主版本號。
interface 修飾符
#Dart 沒有單獨的語法來宣告純介面。相反,你宣告一個抽象類,它恰好只包含抽象方法。當用戶在你的包的 API 中看到該類時,他們可能不知道它是否包含可以透過擴充套件類來複用的程式碼,或者它是否旨在用作介面。
你可以透過在類上放置interface修飾符來澄清這一點。這允許該類在 implements 子句中使用,但阻止它在 extends 中使用。
即使類*確實*有非抽象方法,你可能也希望阻止使用者擴充套件它。繼承是軟體中最強大的耦合形式之一,因為它實現了程式碼複用。但這種耦合也危險且脆弱。當繼承跨越包邊界時,很難在不破壞子類的情況下演進超類。
將類標記為 interface 允許使用者構造它(除非它也標記為 abstract)並實現類的介面,但阻止他們複用其任何程式碼。
當一個類被標記為 interface 時,該限制可以在宣告該類的庫中被忽略。在庫內部,你可以自由地擴充套件它,因為這都是你的程式碼,並且你理所當然地知道自己在做什麼。該限制適用於其他包,甚至你自己的包中的其他庫。
base 修飾符
#base 修飾符與 interface 有些相反。它允許你在 extends 子句中使用該類,或在 with 子句中使用 mixin 或 mixin 類。但是,它禁止類庫之外的程式碼在 implements 子句中使用該類或 mixin。
這確保了作為你的類或 mixin 介面的例項的每個物件都繼承了你的實際實現。特別是,這意味著每個例項都將包含你的類或 mixin 宣告的所有私有成員。這有助於防止可能發生的執行時錯誤。
考慮這個庫:
class A {
void _privateMethod() {
print('I inherited from A');
}
}
void callPrivateMethod(A a) {
a._privateMethod();
}這段程式碼本身看起來沒問題,但沒有什麼能阻止使用者建立另一個這樣的庫:
import 'a.dart';
class B implements A {
// No implementation of _privateMethod()!
}
main() {
callPrivateMethod(B()); // Runtime exception!
}將 base 修飾符新增到類中可以幫助防止這些執行時錯誤。與 interface 一樣,你可以在宣告 base 類或 mixin 的同一庫中忽略此限制。然後,同一庫中的子類將被提醒實現私有方法。但請注意,下一節*確實*適用:
base 傳遞性
#將類標記為 base 的目標是確保該型別的每個例項都具體地繼承自它。為了保持這一點,base 限制具有“傳染性”。標記為 base 的型別的每個子型別——無論是*直接*還是*間接*的——也必須阻止被實現。這意味著它必須標記為 base(或 final 或 sealed,我們接下來會講到)。
因此,對型別應用 base 需要一些注意。它不僅影響使用者如何使用你的類或 mixin,還影響*其*子類可以提供的能力。一旦你將 base 放在一個型別上,其下的整個繼承體系都將禁止被實現。
這聽起來很激烈,但這是大多數其他程式語言一直以來的工作方式。大多數語言根本沒有隱式介面,因此當你在 Java、C# 或其他語言中宣告一個類時,你實際上具有相同的約束。
final 修飾符
#如果你想要 interface 和 base 的所有限制,你可以將類或 mixin 類標記為final。這會阻止庫外部的任何人建立它的任何子型別:不允許在 implements、extends、with 或 on 子句中使用它。
這對類的使用者來說限制最多。他們能做的就是構造它(除非它被標記為 abstract)。作為回報,你作為類維護者的限制最少。你可以新增新方法,將建構函式轉換為工廠建構函式等等,而無需擔心破壞任何下游使用者。
sealed 修飾符
#最後一個修飾符,sealed,是特殊的。它主要用於在模式匹配中啟用窮舉檢查。如果一個 switch 為標記為 sealed 的型別的每個直接子型別都有 case,那麼編譯器就知道該 switch 是窮舉的。
sealed class Amigo {}
class Lucky extends Amigo {}
class Dusty extends Amigo {}
class Ned extends Amigo {}
String lastName(Amigo amigo) => switch (amigo) {
Lucky _ => 'Day',
Dusty _ => 'Bottoms',
Ned _ => 'Nederlander',
};這個 switch 為 Amigo 的每個子型別都有一個 case。編譯器知道 Amigo 的每個例項都必須是這些子型別之一的例項,因此它知道該 switch 是安全窮舉的,並且不需要任何最終的 default case。
為了使其健全,編譯器強制執行兩個限制:
sealed類本身不能直接構造。否則,你可能會有一個Amigo例項,它不是*任何*子型別的例項。因此,每個sealed類也隱式地是abstract。sealed型別的每個直接子型別都必須宣告在與sealed型別相同的庫中。這樣,編譯器可以找到它們所有。它知道沒有其他未匹配任何 case 的隱藏子型別存在。
第二個限制與 final 類似。與 final 一樣,這意味著標記為 sealed 的類不能在宣告它的庫之外直接擴充套件、實現或混合。但是,與 base 和 final 不同,沒有*傳遞性*限制:
sealed class Amigo {}
class Lucky extends Amigo {}
class Dusty extends Amigo {}
class Ned extends Amigo {}// This is an error:
class Bad extends Amigo {}
// But these are both fine:
class OtherLucky extends Lucky {}
class OtherDusty implements Dusty {}當然,如果你*希望*你的 sealed 型別的子型別也受到限制,你可以透過使用 interface、base、final 或 sealed 標記它們來實現。
sealed 與 final 的比較
#如果你有一個不希望使用者直接子型別化的類,那麼你何時應該使用 sealed 而不是 final 呢?一些簡單的規則:
如果你希望使用者能夠直接構造類的例項,那麼它*不能*使用
sealed,因為sealed型別是隱式抽象的。如果你的庫中沒有子型別,那麼使用
sealed毫無意義,因為你無法獲得窮舉檢查的好處。
否則,如果類確實有一些你定義的子型別,那麼 sealed 可能就是你想要的。如果使用者看到類有幾個子型別,那麼能夠將它們分別作為 switch case 處理並讓編譯器知道整個型別都已覆蓋是非常方便的。
使用 sealed 確實意味著如果你稍後向庫中新增另一個子型別,這將是破壞性 API 變更。當一個新的子型別出現時,所有現有的 switch 都會變得非窮舉,因為它們不處理新型別。這就像向列舉中新增新值一樣。
這些非窮舉 switch 編譯錯誤對使用者*有用*,因為它們能引起使用者對其程式碼中需要處理新型別的地方的注意。
但這確實意味著每當你新增一個新的子型別,都是一個破壞性變更。如果你想以非破壞性的方式新增新子型別,那麼最好使用 final 而不是 sealed 標記超型別。這意味著當用戶對該超型別的值進行 switch 操作時,即使他們有所有子型別的 case,編譯器也會強制他們新增另一個 default case。如果你以後新增更多子型別,那麼將執行該 default case。
總結
#作為 API 設計者,這些新修飾符讓你能夠控制使用者如何使用你的程式碼,反之,你也可以在不破壞使用者程式碼的情況下演進你的程式碼。
但這些選項也帶來了複雜性:作為 API 設計者,你現在有更多的選擇。此外,由於這些功能是新的,我們仍然不知道最佳實踐是什麼。每種語言的生態系統都不同,有不同的需求。
幸運的是,你不需要一下子弄清楚所有事情。我們特意選擇了預設值,即使你不做任何事情,你的類也大多具有 3.0 之前所提供的相同能力。如果你只是想保持你的 API 不變,將 mixin 放在已經支援該功能的類上,你就完成了。
隨著時間的推移,當你對需要更精細控制的地方有所瞭解時,你可以考慮應用其他一些修飾符:
使用
interface阻止使用者複用你的類程式碼,同時允許他們重新實現其介面。使用
base要求使用者複用你的類程式碼,並確保你的類型別的每個例項都是該實際類或子類的例項。使用
final完全阻止類被擴充套件。使用
sealed選擇對子型別族進行窮舉檢查。
當你這樣做時,在釋出包時遞增主版本號,因為這些修飾符都意味著是破壞性變更的限制。