內容

延伸型別是一種編譯時期抽象,它使用不同的純靜態介面「包裝」現有型別。它們是 靜態 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);
}

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

成員

#

在擴充類型主體中宣告成員,以定義其介面,方式與類別成員相同。擴充類型成員可以是方法、取得器、設定器或運算子(不允許 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 取得器和 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. 提供現有類型的不同介面

#

透明 的擴充類型(不會 實作 它的表示類型)在靜態上被視為一個全新的類型,與它的表示類型不同。您不能將它指定給它的表示類型,而且它不會公開它的表示類型的成員。

例如,使用 用法 宣告的 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> 完全相同。

換句話說,真正的包裝類別可以封裝包裝物件,而擴充類型只是包裝物件的編譯時期檢視。雖然真正的包裝比較安全,但權衡取捨是擴充類型讓您有選項可以避免包裝物件,這可以在某些場景中大幅提升效能。