內容

供 API 維護人員使用的類別修飾詞

Dart 3.0 新增了一些 新修飾詞,您可以將這些修飾詞置於類別和 混入宣告 中。如果您是函式庫套件的作者,這些修飾詞可讓您更能控制使用者使用套件所匯出的類型時可以執行的動作。這可以讓您更輕鬆地演進套件,以及更輕鬆地知道變更您的程式碼是否可能會中斷使用者。

Dart 3.0 也包含一個 重大變更,與將類別當作混入使用有關。這個變更可能不會中斷您的類別,但可能會中斷使用您類別的使用者。

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

類別上的 mixin 修飾詞

#

最重要的修飾詞是 mixin。Dart 3.0 之前的語言版本允許任何類別在其他類別的 with 子句中當作混入使用,除非 該類別

  • 宣告任何非建構函式的建構函式。
  • 繼承 Object 以外的任何類別。

這使得在類別中新增建構函式或 extends 子句時,很容易意外中斷別人的程式碼,因為您沒有意識到其他人正在 with 子句中使用它。

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

dart
mixin class Both {}

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

如果您將套件更新至 Dart 3.0,而且沒有變更任何程式碼,您可能不會看到任何錯誤。但是,如果使用者將您的類別當作混入使用,您可能會不經意地中斷他們。

將類別遷移為 mixin

#

如果類別有非建構函式的建構函式、extends 子句或 with 子句,則它已經無法當作混入使用。行為不會隨著 Dart 3.0 而改變;您不必擔心,也不需要採取任何措施。

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

以下幾個問題可以幫助您做出決定。第一個是務實的

  • 您是否願意冒著中斷任何使用者的風險?如果答案是堅決的「不」,請在任何和所有 可以當作混入使用 的類別之前加上 mixin。這可以完全保留 API 的現有行為。

另一方面,如果您想趁此機會重新思考您的 API 提供的功能,那麼您可能不會將它變成 mixin class。考量以下兩個設計問題

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

  • 希望人們能夠將宣告用作混入嗎?換句話說,您希望他們能夠在 with 句中使用它嗎?

如果兩個問題的答案都是「是」,那麼就將它設為混入類別。如果第二個問題的答案是「否」,那麼就將它保留為類別。如果第一個問題的答案是「否」,而第二個問題的答案是「是」,那麼就將它從類別變更為混入宣告。

最後兩個選項,將它保留為類別或將它轉換為純混入,都是會中斷 API 的變更。如果您執行此操作,您將需要提升套件的主要版本。

其他選擇加入的修飾詞

#

將類別視為混入處理是 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 宣告的所有私人成員。這有助於防止可能發生的執行時期錯誤。

考慮這個函式庫

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(或 finalsealed,我們稍後會說明)。

因此,將 base 套用至類型需要小心。它不僅會影響使用者可以對你的類別或 mixin 做什麼,還會影響他們的子類別可以提供的功能。一旦你對類型套用 base,其下的整個階層就禁止被實作。

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

final 修飾詞

#

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

對類別的使用者來說,這是最具限制性的。他們唯一能做的就是建構它(除非它標記為 abstract)。作為類別維護者,相對地你的限制最少。你可以新增方法,將建構函式轉換為工廠建構函式等,而不用擔心會中斷任何下游使用者。

sealed 修飾詞

#

最後一個修飾詞 sealed 很特別。它的存在主要是為了在模式配對中啟用 窮舉檢查。如果一個 switch 有標記為 sealed 的類型的每個直接子類型的案例,那麼編譯器就知道這個 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 的每個子類型的案例。編譯器知道 Amigo 的每個實例都必須是這些子類型的其中一個實例,因此它知道這個 switch 是安全窮舉的,不需要任何最終的預設案例。

為了讓這一點成立,編譯器強制執行兩個限制

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

  2. sealed 類型的每個直接子類型都必須與宣告 sealed 類型的同一個函式庫中。這樣,編譯器才能找到它們。它知道沒有其他隱藏的子類型會與任何案例不匹配。

第二個限制類似於 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 案例處理並讓編譯器知道整個類型都被涵蓋是很方便的。

使用 sealed 確實表示如果你稍後在函式庫中新增另一個子類型,這是一個中斷 API 的變更。當一個新的子類型出現時,所有那些現有的 switch 都會變成非窮舉的,因為它們不處理新的類型。這就像在列舉中新增一個新值一樣。

這些非窮舉的 switch 編譯錯誤對使用者而言是有用的,因為它們可以讓使用者注意其程式碼中需要處理新類型的部分。

但這表示每當您新增新的子類型時,這會是一個重大變更。如果您想要以非重大變更的方式新增新的子類型,那麼最好使用 final 而非 sealed 來標記超類型。這表示當使用者針對該超類型的值進行切換時,即使他們已針對所有子類型設定案例,編譯器仍會強制他們新增另一個預設案例。如果您稍後新增更多子類型,則預設案例將會執行。

摘要

#

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

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

幸運的是,您不必一次搞清楚所有事情。我們刻意選擇預設值,因此即使您什麼都不做,您的類別大多數仍具有 3.0 之前相同的可用性。如果您只想讓您的 API 保持原樣,請將 mixin 放置在已支援該功能的類別上,這樣就完成了。

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

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

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

  • 使用 final 來完全防止類別被延伸。

  • 使用 sealed 來選擇對子類型的系列進行窮舉性檢查。

當您這麼做時,請在發佈您的套件時增加主要版本,因為這些修飾詞都暗示了會造成重大變更的限制。