內容

擴充類型

擴充類型是一種編譯時抽象化,它使用不同的靜態介面「封裝」現有類型。它們是靜態 JS 互通的主要組成部分,因為它們可以輕鬆修改現有類型的介面(對於任何形式的互通都至關重要),而不會產生實際包裝器的成本。

擴充類型對基礎類型(稱為表示類型)的物件可用的操作集(或介面)強制執行紀律。在定義擴充類型的介面時,您可以選擇重複使用表示類型的某些成員、省略其他成員、取代其他成員,以及新增新功能。

下列範例封裝 int 類型,以建立僅允許對 ID 編號有意義之操作的擴充類型

dart
extension type IdNumber(int id) {
  // Wraps the 'int' type's '<' operator:
  operator <(IdNumber other) => id < other.id;
  // Doesn't declare the '+' operator, for example,
  // because addition does not make sense for ID numbers.
}

void main() {
  // Without the discipline of an extension type,
  // 'int' exposes ID numbers to unsafe operations:
  int myUnsafeId = 42424242;
  myUnsafeId = myUnsafeId + 10; // This works, but shouldn't be allowed for IDs.

  var safeId = IdNumber(42424242);
  safeId + 10; // Compile-time error: No '+' operator.
  myUnsafeId = safeId; // Compile-time error: Wrong type.
  myUnsafeId = safeId as int; // OK: Run-time cast to representation type.
  safeId < IdNumber(42424241); // OK: Uses wrapped '<' operator.
}

語法

#

宣告

#

使用 extension type 宣告和名稱定義新的擴充類型,然後以括號括住表示類型宣告

dart
extension type E(int i) {
  // Define set of operations.
}

表示類型宣告 (int i) 指定擴充類型 E 的基礎類型為 int,並且表示物件的參考名稱為 i。此宣告也引入

  • 表示物件的隱含 getter,其傳回類型為表示類型:int get i
  • 隱含建構子:E(int i) : i = i

表示物件讓擴充類型可以存取基礎類型的物件。物件在擴充類型主體範圍內,您可以使用其名稱作為 getter 來存取它

  • 在擴充類型主體內使用 i(或在建構子中使用 this.i)。
  • 在外部使用屬性擷取 e.i(其中 e 的靜態類型為擴充類型)。

擴充類型宣告也可以包含類型參數,就像類別或擴充一樣

dart
extension type E<T>(List<T> elements) {
  // ...
}

建構子

#

您可以選擇性地在擴充類型的主體中宣告建構子。表示宣告本身是隱含的建構子,因此預設會取代擴充類型的未命名建構子。任何其他非重新導向產生式建構子都必須在其初始化清單或形式參數中使用 this.i 初始化表示物件的執行個體變數。

dart
extension type E(int i) {
  E.n(this.i);
  E.m(int j, String foo) : i = j + foo.length;
}

void main() {
  E(4); // Implicit unnamed constructor.
  E.n(3); // Named constructor.
  E.m(5, "Hello!"); // Named constructor with additional parameters.
}

或者,您可以命名表示宣告建構子,在這種情況下,主體中會有未命名的建構子空間

dart
extension type const E._(int it) {
  E(): this._(42);
  E.otherName(this.it);
}

void main2() {
  E();
  const E._(2);
  E.otherName(3);
}

您也可以使用與類別相同的私用建構子語法 _ 完全隱藏建構子,而不只是定義新的建構子。例如,如果您只希望用戶端使用 String 建構 E,即使基礎類型是 int

dart
extension type E._(int i) {
  E.fromString(String foo) : i = int.parse(foo);
}

您也可以宣告轉送產生式建構子,或工廠建構子(也可以轉送至子擴充類型的建構子)。

成員

#

在擴充類型的主體中宣告成員,以定義其介面,方式與類別成員相同。擴充類型成員可以是方法、getter、setter 或運算子(不允許非external 執行個體變數抽象成員

dart
extension type NumberE(int value) {
  // Operator:
  NumberE operator +(NumberE other) =>
      NumberE(value + other.value);
  // Getter:
  NumberE get myNum => this;
  // Method:
  bool isValid() => !value.isNegative;
}

表示類型的介面成員預設不是擴充類型的介面成員。若要使表示類型的單一成員在擴充類型上可用,您必須在擴充類型定義中為其撰寫宣告,例如 NumberE 中的 operator +。您也可以定義與表示類型無關的新成員,例如 i getter 和 isValid 方法。

實作

#

您可以選擇性地使用 implements 子句來

  • 在擴充類型上引入子類型關係,以及
  • 將表示物件的成員新增至擴充類型介面。

implements 子句引入了適用性關係,類似於擴充方法與其 on 類型之間的關係。適用於超類型的成員也適用於子類型,除非子類型具有相同成員名稱的宣告。

擴充類型只能實作

  • 其表示類型。這會使表示類型的所有成員隱含地對擴充類型可用。

    dart
    extension type NumberI(int i) 
      implements int{
      // 'NumberI' can invoke all members of 'int',
      // plus anything else it declares here.
    }
  • 其表示類型的超類型。這會使超類型的成員可用,但不一定使表示類型的所有成員可用。

    dart
    extension type Sequence<T>(List<T> _) implements Iterable<T> {
      // Better operations than List.
    }
    
    extension type Id(int _id) implements Object {
      // Makes the extension type non-nullable.
      static Id? tryParse(String source) => int.tryParse(source) as Id?;
    }
  • 在相同表示類型上有效的另一個擴充類型。這可讓您在多個擴充類型中重複使用操作(類似於多重繼承)。

    dart
    extension type const Opt<T>._(({T value})? _) { 
      const factory Opt(T value) = Val<T>;
      const factory Opt.none() = Non<T>;
    }
    extension type const Val<T>._(({T value}) _) implements Opt<T> { 
      const Val(T value) : this._((value: value));
      T get value => _.value;
    }
    extension type const Non<T>._(Null _) implements Opt<Never> {
      const Non() : this._(null);
    }

閱讀用法章節,以深入瞭解 implements 在不同情境中的效果。

@redeclare

#

宣告與超類型的成員共用名稱的擴充類型成員不是像類別之間那樣的覆寫關係,而是重新宣告。擴充類型成員宣告完全取代任何具有相同名稱的超類型成員。無法為相同的函式提供替代實作。

您可以使用 @redeclare 註解來告知編譯器您有意選擇使用與超類型成員相同的名稱。如果這實際上不是真的,例如如果其中一個名稱的輸入錯誤,分析器將會警告您。

dart
extension type MyString(String _) implements String {
  // Replaces 'String.operator[]'
  @redeclare
  int operator [](int index) => codeUnitAt(index);
}

您也可以啟用 lint annotate_redeclares,如果在您宣告隱藏超介面成員且使用 @redeclare 註解的擴充類型方法時,會收到警告。

用法

#

若要使用擴充類型,請建立執行個體,就像使用類別一樣:透過呼叫建構子

dart
extension type NumberE(int value) {
  NumberE operator +(NumberE other) =>
      NumberE(value + other.value);

  NumberE get next => NumberE(value + 1);
  bool isValid() => !value.isNegative;
}

void testE() { 
  var num = NumberE(1);
}

然後,您可以在物件上叫用成員,就像使用類別物件一樣。

擴充類型有兩種同樣有效,但本質上不同的核心使用案例

  1. 為現有類型提供擴充介面。
  2. 為現有類型提供不同的介面。

1. 為現有類型提供擴充介面

#

當擴充類型實作其表示類型時,您可以將其視為「透明」,因為它允許擴充類型「看到」基礎類型。

透明擴充類型可以叫用表示類型(不是重新宣告)的所有成員,以及它定義的任何輔助成員。這會為現有類型建立新的擴充介面。新的介面適用於其靜態類型為擴充類型的運算式。

這表示你可以調用表示類型的成員(不像非透明的擴展類型),就像這樣

dart
extension type NumberT(int value) 
  implements int {
  // Doesn't explicitly declare any members of 'int'.
  NumberT get i => this;
}

void main () {
  // All OK: Transparency allows invoking `int` members on the extension type:
  var v1 = NumberT(1); // v1 type: NumberT
  int v2 = NumberT(2); // v2 type: int
  var v3 = v1.i - v1;  // v3 type: int
  var v4 = v2 + v1; // v4 type: int
  var v5 = 2 + v1; // v5 type: int
  // Error: Extension type interface is not available to representation type
  v2.i;
}

你也可以擁有一個「大部分透明」的擴展類型,它會新增新的成員並透過重新宣告超類別中給定的成員名稱來調整其他成員。例如,這可以讓你對方法的某些參數使用更嚴格的類型,或使用不同的預設值。

另一種大部分透明的擴展類型方法是實作一個作為表示類型的超類別的類型。例如,如果表示類型是私有的,但其超類別定義了對客戶端來說重要的介面部分。

2. 為現有類型提供不同的介面

#

不是透明的(不implement其表示類型)的擴展類型在靜態上被視為一個完全新的類型,與其表示類型不同。你不能將其分配給其表示類型,而且它不會公開其表示類型的成員。

例如,以我們在用法下宣告的 NumberE 擴展類型為例

dart
void testE() { 
  var num1 = NumberE(1);
  int num2 = NumberE(2); // Error: Can't assign 'NumberE' to 'int'.
  
  num1.isValid(); // OK: Extension member invocation.
  num1.isNegative(); // Error: 'NumberE' does not define 'int' member 'isNegative'.
  
  var sum1 = num1 + num1; // OK: 'NumberE' defines '+'.
  var diff1 = num1 - num1; // Error: 'NumberE' does not define 'int' member '-'.
  var diff2 = num1.value - 2; // OK: Can access representation object with reference.
  var sum2 = num1 + 2; // Error: Can't assign 'int' to parameter type 'NumberE'. 
  
  List<NumberE> numbers = [
    NumberE(1), 
    num1.next, // OK: 'next' getter returns type 'NumberE'.
    1, // Error: Can't assign 'int' element to list type 'NumberE'.
  ];
}

你可以用這種方式使用擴展類型來取代現有類型的介面。這允許你為你的新類型的約束建模一個有意義的介面(就像引言中的 IdNumber 範例),同時也能受益於簡單預定義類型(如 int)的效能和便利性。

這種用例最接近於對包裝類別的完整封裝(但實際上只是一個有些受保護的抽象)。

類型考量

#

擴展類型是一種編譯時的包裝結構。在執行時,完全沒有擴展類型的蹤跡。任何類型查詢或類似的執行時操作都會在表示類型上運作。

這使得擴展類型成為一種不安全的抽象,因為你始終可以在執行時找出表示類型並存取底層物件。

動態類型測試 (e is T)、轉換 (e as T) 和其他執行時類型查詢(例如 switch (e) ...if (e case ...))都會評估為底層的表示物件,並根據該物件的執行時類型進行類型檢查。當 e 的靜態類型是擴展類型,以及針對擴展類型進行測試時 (case MyExtensionType(): ...),情況都是如此。

dart
void main() {
  var n = NumberE(1);

  // Run-time type of 'n' is representation type 'int'.
  if (n is int) print(n.value); // Prints 1.

  // Can use 'int' methods on 'n' at run time.
  if (n case int x) print(x.toRadixString(10)); // Prints 1.
  switch (n) {
    case int(:var isEven): print("$n (${isEven ? "even" : "odd"})"); // Prints 1 (odd).
  }
}

同樣地,在此範例中,匹配值的靜態類型是擴展類型的靜態類型

dart
void main() {
  int i = 2;
  if (i is NumberE) print("It is"); // Prints 'It is'.
  if (i case NumberE v) print("value: ${v.value}"); // Prints 'value: 2'.
  switch (i) {
    case NumberE(:var value): print("value: $value"); // Prints 'value: 2'.
  }
}

在使用擴展類型時,務必留意這種特性。始終記住,擴展類型在編譯時存在且重要,但在編譯期間會被擦除。

例如,考慮一個靜態類型為擴展類型 E 的表達式 e,且 E 的表示類型為 R。然後,e 的值的執行時類型是 R 的子類型。即使類型本身也被擦除了;List<E> 在執行時與 List<R> 完全相同。

換句話說,一個真正的包裝類別可以封裝一個被包裝的物件,而擴展類型只是一個在編譯時對被包裝物件的檢視。雖然真正的包裝更安全,但權衡之下,擴展類型讓你選擇避免使用包裝物件,這可以在某些情況下大幅提升效能。