跳到主要內容

API 維護者的類別修飾詞

Dart 3.0 新增了一些您可以放在類別和 mixin 宣告上的新修飾詞。如果您是程式庫套件的作者,這些修飾詞可讓您更精確地控制使用者對您的套件所匯出類型可執行的操作。這可以讓您更輕鬆地演進套件,並且更容易知道對程式碼的變更是否會對使用者造成破壞。

Dart 3.0 也包含一個關於將類別作為 mixin 使用的重大變更。此變更可能不會破壞您的類別,但可能會破壞您類別的使用者

本指南將引導您了解這些變更,讓您知道如何使用新的修飾詞,以及它們如何影響您的程式庫使用者。

類別上的 mixin 修飾詞

#

最需要注意的修飾詞是 mixin。在 Dart 3.0 之前的語言版本中,任何類別都可以用作另一個類別 with 子句中的 mixin,除非該類別

  • 宣告任何非 factory 建構子。
  • 擴充 Object 以外的任何類別。

這使得在不了解其他人正在 with 子句中使用類別的情況下,透過新增建構子或 extends 子句到類別而意外破壞其他人的程式碼變得容易。

Dart 3.0 不再允許預設將類別用作 mixin。相反地,您必須透過宣告 mixin class 來明確選擇加入該行為

dart
mixin class Both {}

class UseAsMixin with Both {}
class UseAsSuperclass extends Both {}

如果您將套件更新至 Dart 3.0 且未變更任何程式碼,您可能不會看到任何錯誤。但是,如果您的套件使用者正在將您的類別用作 mixin,您可能會在無意間破壞他們的使用。

將類別作為 mixin 移轉

#

如果類別具有非 factory 建構子、extends 子句或 with 子句,則它已經不能用作 mixin。行為在 Dart 3.0 中不會變更;沒有什麼好擔心的,也不需要做任何事。

實際上,這描述了大約 90% 的現有類別。對於其餘可用作 mixin 的類別,您必須決定要支援什麼。

以下是一些有助於決定的問題。第一個是務實的

  • 您想要承擔破壞任何使用者的風險嗎? 如果答案是堅決的「否」,則在任何和所有可能用作 mixin 的類別之前放置 mixin。這完全保留了 API 的現有行為。

另一方面,如果您想藉此機會重新思考 API 提供的功能,則您可能想將其變成 mixin class。請考慮以下兩個設計問題

  • 您是否希望使用者能夠直接建構它的實例? 換句話說,該類別是否刻意不是抽象的?

  • 想要人們能夠將宣告用作 mixin 嗎? 換句話說,您是否希望他們能夠在 with 子句中使用它?

如果兩個問題的答案都是「是」,則將其設為 mixin class。如果第二個問題的答案是「否」,則只需將其保留為類別。如果第一個問題的答案是「否」而第二個問題的答案是「是」,則將其從類別變更為 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 class。但是,它不允許類別程式庫外部的程式碼在 implements 子句中使用該類別或 mixin。

這確保了作為您的類別或 mixin 介面實例的每個物件都繼承了您的實際實作。特別是,這表示每個實例都將包含您的類別或 mixin 宣告的所有私有成員。這有助於防止可能發生的執行階段錯誤。

考慮這個程式庫

a.dart
dart
class A {
  void _privateMethod() {
    print('I inherited from A');
  }
}

void callPrivateMethod(A a) {
  a._privateMethod();
}

這段程式碼本身看起來不錯,但沒有任何東西可以阻止使用者建立另一個像這樣的程式庫

b.dart
dart
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 (或 finalsealed,我們稍後會介紹)。

然後,將 base 應用於類型需要謹慎。它不僅會影響使用者可以對您的類別或 mixin 執行的操作,還會影響他們的子類別可以提供的功能。一旦您在類型上放置 base,其下的整個階層結構都將被禁止實作。

這聽起來很強烈,但這是大多數其他程式設計語言一直以來的運作方式。大多數語言根本沒有隱式介面,因此當您在 Java、C# 或其他語言中宣告類別時,您實際上具有相同的限制。

final 修飾詞

#

如果您想要 interfacebase 的所有限制,您可以將類別或 mixin class 標記為 final。這會防止您程式庫外部的任何人建立它的任何子類型:禁止在 implementsextendswithon 子句中使用它。

這是對類別使用者最嚴格的限制。他們唯一可以做的就是建構它 (除非它標記為 abstract)。作為回報,作為類別維護者,您的限制最少。您可以新增方法、將建構子變成 factory 建構子等等,而無需擔心破壞任何下游使用者。

sealed 修飾詞

#

最後一個修飾詞 sealed 很特別。它主要用於在模式比對中啟用詳盡性檢查。如果 switch 具有標記為 sealed 的類型的每個直接子類型的 case,則編譯器知道該 switch 是詳盡的。

amigos.dart
dart
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。

為了使這一切合理,編譯器強制執行兩個限制

  1. sealed 類別本身不能直接建構。否則,您可能會擁有一個不是任何子類型實例的 Amigo 實例。因此,每個 sealed 類別也隱含地是 abstract

  2. sealed 類型的每個直接子類型都必須與宣告 sealed 類型的程式庫在同一個程式庫中。這樣,編譯器就可以找到它們全部。它知道沒有其他隱藏的子類型在周圍漂浮,而這些子類型將不符合任何 case。

第二個限制與 final 類似。與 final 一樣,這表示標記為 sealed 的類別不能在宣告它的程式庫外部直接擴充、實作或混入。但是,與 basefinal 不同,沒有傳遞限制

amigo.dart
dart
sealed class Amigo {}
class Lucky extends Amigo {}
class Dusty extends Amigo {}
class Ned extends Amigo {}
other.dart
dart
// This is an error:
class Bad extends Amigo {}

// But these are both fine:
class OtherLucky extends Lucky {}
class OtherDusty implements Dusty {}

當然,如果您想要也限制 sealed 類型的子類型,您可以透過使用 interfacebasefinalsealed 來標記它們來實現。

sealedfinal 的比較

#

如果您有一個不希望使用者能夠直接建立子類型的類別,那麼您應該在何時使用 sealedfinal?幾個簡單的規則

  • 如果您希望使用者能夠直接建構類別的實例,那麼它不能使用 sealed,因為 sealed 類型隱含地是抽象的。

  • 如果類別在您的程式庫中沒有子類型,那麼使用 sealed 就沒有意義,因為您不會獲得詳盡性檢查的好處。

否則,如果類別確實有一些您定義的子類型,則 sealed 很可能是您想要的。如果使用者看到類別有一些子類型,則能夠將它們各自作為 switch case 處理,並讓編譯器知道涵蓋了整個類型,這將會很方便。

使用 sealed 確實表示,如果您稍後在程式庫中新增另一個子類型,則這會是一個破壞性的 API 變更。當出現新的子類型時,所有這些現有的 switch 都會變成非詳盡的,因為它們不處理新的類型。這與向列舉新增新值完全一樣。

這些非詳盡的 switch 編譯錯誤對使用者很有用,因為它們將使用者的注意力吸引到他們程式碼中需要處理新類型的位置。

但這確實表示,每當您新增新的子類型時,這都是一個破壞性的變更。如果您想要以非破壞性的方式自由新增新的子類型,那麼最好使用 final 而不是 sealed 來標記父類型。這表示,當使用者在該父類型的值上進行 switch 時,即使他們具有所有子類型的 case,編譯器也會強制他們新增另一個 default case。然後,如果您稍後新增更多子類型,則將會執行該 default case。

摘要

#

作為 API 設計者,這些新的修飾詞讓您可以控制使用者如何使用您的程式碼,反之亦然,您可以如何在不破壞使用者程式碼的情況下演進您的程式碼。

但是這些選項也帶來了複雜性:您現在作為 API 設計者需要做出更多選擇。此外,由於這些功能是新的,我們仍然不知道最佳實務會是什麼。每個語言的生態系統都不同,並且有不同的需求。

幸運的是,您不需要一次性解決所有問題。我們特意選擇了預設值,以便即使您什麼都不做,您的類別也大致具有與 3.0 之前相同的功能。如果您只想保持 API 原樣,請在已經支援 mixin 的類別上放置 mixin,這樣就完成了。

隨著時間的推移,當您了解自己想要更精細的控制時,您可以考慮應用其他一些修飾詞

  • 使用 interface 來防止使用者重複使用您類別的程式碼,同時允許他們重新實作其介面。

  • 使用 base 來要求使用者重複使用您類別的程式碼,並確保您類別類型的每個實例都是該實際類別或子類別的實例。

  • 使用 final 來完全防止類別被擴充。

  • 使用 sealed 來選擇加入一組子類型的詳盡性檢查。

當您執行此操作時,發布套件時請增加主要版本號,因為這些修飾詞都暗示了破壞性變更的限制。