跳至主要內容

擴充類型

擴充類型是一種編譯時抽象化,它用不同的、僅限靜態的介面「包裝」現有類型。它們是靜態 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);
}

如果您宣告隱藏超介面成員且未以 @redeclare 註解的擴充類型方法,您也可以啟用 lint annotate_redeclares 以取得警告。

用法

#

若要使用擴充類型,請建立實例,就像您對類別所做的那樣:透過呼叫建構子

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. 為現有類型提供不同的介面

#

非透明 (未 實作 其表示類型) 的擴充類型在靜態上被視為與其表示類型完全不同的新類型。您無法將其指派給其表示類型,並且它不會公開其表示類型的成員。

例如,採用我們在 用法 下宣告的 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) 的效能和便利性。

此用例盡可能接近 wrapper 類別的完整封裝 (但實際上只是一種 稍微受保護的 抽象化)。

類型考量

#

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

這使得擴充類型成為不安全的抽象化,因為您始終可以在執行階段找出表示類型並存取基礎物件。

動態類型測試 (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> 完全相同。

換句話說,真正的 wrapper 類別可以封裝包裝的物件,而擴充類型只是包裝物件的編譯時視圖。雖然真正的 wrapper 更安全,但權衡取捨是擴充類型讓您可以選擇避免 wrapper 物件,這可以在某些情境中大幅提升效能。