API 維護者的類別修飾詞
Dart 3.0 新增了一些您可以放置在類別和 mixin 宣告上的新修飾詞。如果您是程式庫套件的作者,這些修飾詞可讓您對使用者可對您的套件匯出的類型執行哪些操作擁有更多控制權。這可以讓您的套件更容易發展,並更容易知道對程式碼的變更是否可能會對使用者造成破壞。
Dart 3.0 還包含一個圍繞使用類別作為 mixins 的重大變更。此變更可能不會破壞您的類別,但可能會破壞您類別的使用者。
本指南將引導您瞭解這些變更,以便您知道如何使用新的修飾詞,以及它們如何影響您程式庫的使用者。
類別上的 mixin
修飾詞
#最需要注意的修飾詞是 mixin
。Dart 3.0 之前的語言版本允許任何類別在另一個類別的 with
子句中用作 mixin,除非該類別
- 宣告任何非 factory 建構子。
- 擴充
Object
以外的任何類別。
這使得很容易在沒有意識到其他人正在 with
子句中使用它的情況下,透過新增建構子或 extends
子句到類別,意外地破壞其他人的程式碼。
Dart 3.0 預設不再允許將類別用作 mixins。相反,您必須透過宣告 mixin class
明確選擇啟用該行為
mixin class Both {}
class UseAsMixin with Both {}
class UseAsSuperclass extends Both {}
如果您將套件更新至 Dart 3.0 且沒有變更任何程式碼,您可能不會看到任何錯誤。但如果您的套件的使用者正在使用您的類別作為 mixins,您可能會在無意中破壞他們的使用。
將類別遷移為 mixins
#如果類別具有非 factory 建構子、extends
子句或 with
子句,則它已經不能用作 mixin。行為不會因 Dart 3.0 而變更;沒有什麼好擔心的,也沒有什麼需要做的。
在實務上,這描述了大約 90% 的現有類別。對於其餘可以作為 mixin 使用的類別,您必須決定要支援什麼。
以下是一些有助於決定的問題。第一個是務實的
- 您是否想要承擔破壞任何使用者的風險?如果答案是堅決的「否」,則請在任何和所有可能作為 mixin 使用的類別之前放置
mixin
。這完全保留了您 API 的現有行為。
另一方面,如果您想藉此機會重新思考您的 API 提供的功能,那麼您可能不想將其變成 mixin class
。請考慮以下兩個設計問題
您是否希望使用者能夠直接建構其實例?換句話說,該類別是否故意不是抽象的?
您希望人們能夠將宣告用作 mixin 嗎?換句話說,您是否希望他們能夠在
with
子句中使用它?
如果兩個問題的答案都是「是」,則使其成為 mixin 類別。如果第二個問題的答案是「否」,則將其保留為類別。如果第一個問題的答案是「否」且第二個問題的答案是「是」,則將其從類別變更為 mixin 宣告。
最後兩個選項,將其保留為類別或將其轉換為純 mixin,都是破壞性的 API 變更。如果您執行此操作,您會想要遞增套件的主要版本。
其他選擇性修飾詞
#處理作為 mixins 的類別是 Dart 3.0 中唯一影響您套件 API 的關鍵變更。一旦您做到這一步,如果您不想對您的套件允許使用者執行的操作進行其他變更,您可以停止。
請注意,如果您繼續並使用下述的任何修飾詞,這可能會對您套件的 API 造成破壞性變更,因此需要遞增主要版本。
interface
修飾詞
#Dart 沒有單獨的語法來宣告純介面。相反地,您宣告一個剛好只包含抽象方法的抽象類別。當使用者在您的套件 API 中看到該類別時,他們可能不知道它是否包含可以透過擴充類別來重複使用的程式碼,或者它是否應該用作介面。
您可以透過將interface
修飾詞放在類別上來澄清這一點。這允許該類別在 implements
子句中使用,但防止它在 extends
中使用。
即使類別確實具有非抽象方法,您也可能想要防止使用者擴充它。繼承是軟體中最强大的耦合類型之一,因為它可以重複使用程式碼。但這種耦合也危險且脆弱。當繼承跨越套件界限時,很難在不破壞子類別的情況下發展超類別。
將類別標記為 interface
可讓使用者建構它(除非它也被標記為 abstract
)並實作類別的介面,但防止他們重複使用其任何程式碼。
當類別標記為 interface
時,可以在宣告類別的程式庫內忽略該限制。在程式庫內部,您可以自由擴充它,因為它都是您的程式碼,而且您應該知道自己在做什麼。該限制適用於其他套件,甚至是您自己的套件中的其他程式庫。
base
修飾詞
#base
修飾詞在某種程度上與 interface
相反。它允許你在 extends
子句中使用類別,或在 with
子句中使用混入 (mixin) 或混入類別。但是,它禁止類別程式庫外部的程式碼在 implements
子句中使用該類別或混入。
這確保了你的類別或混入介面的每個實例物件都會繼承你的實際實作。特別是,這表示每個實例都將包含你的類別或混入所宣告的所有私有成員。這有助於防止可能發生的執行階段錯誤。
考慮這個程式庫
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
類別或混入的同一程式庫中忽略此限制。然後,同一程式庫中的子類別會被提醒實作私有方法。但請注意,下一節確實適用
Base 的傳遞性
#將類別標記為 base
的目的是確保該類型的每個實例都具體地從它繼承。為了維持這一點,base 限制是「會傳染的」。標記為 base
的類型的每個子類型——直接或間接——也必須防止被實作。這表示它必須標記為 base
(或 final
或 sealed
,我們將在接下來討論)。
因此,將 base
應用於類型需要小心。它不僅影響使用者可以對你的類別或混入做什麼,還影響他們的子類別可以提供的功能。一旦你在類型上放置了 base
,其下的整個層級結構都被禁止實作。
這聽起來很強烈,但這是大多數其他程式語言一直以來的運作方式。大多數程式語言根本沒有隱式介面,因此當你在 Java、C# 或其他語言中宣告類別時,實際上會具有相同的限制。
final
修飾詞
#如果你想要 interface
和 base
的所有限制,你可以將類別或混入類別標記為 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。
為了使這合理,編譯器強制執行兩個限制
密封類別本身不能直接建構。否則,你可能會有一個不是任何子類型實例的
Amigo
實例。因此,每個sealed
類別也都隱式地是abstract
。密封類型的每個直接子類型都必須在宣告密封類型的同一個程式庫中。這樣,編譯器才能找到它們。它知道不會有其他隱藏的子類型存在,而這些子類型與任何 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 {}
當然,如果你想要限制密封類型的子類型,你可以透過使用 interface
、base
、final
或 sealed
來標記它們來實現。
sealed
與 final
#如果你有一個不希望使用者能夠直接建立子類型的類別,那你應該何時使用 sealed
與 final
?有幾個簡單的規則
如果你希望使用者能夠直接建構類別的實例,則它不能使用
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
來選擇加入一組子類型的詳盡性檢查。
當你這樣做時,在發佈套件時請增加主要版本,因為這些修飾詞都表示會產生破壞性變更的限制。
除非另有說明,否則本網站上的文件反映了 Dart 3.6.0 版本。頁面最後更新於 2024-02-07。 檢視原始碼 或 回報問題。