跳到主要內容

擴充套件型別

擴充套件型別是一種編譯時抽象,它用不同的、僅靜態的介面“包裝”現有型別。它們是 靜態 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

#

宣告一個與超型別成員同名的擴充套件型別成員不是像類之間那樣的覆蓋關係,而是重新宣告。擴充套件型別成員宣告完全替換任何同名的超型別成員。不可能為同一函式提供替代實現。

您可以使用 package:meta 中的 @redeclare 註解來告訴編譯器您有意選擇使用與超型別成員相同的名稱。如果實際上並非如此,例如其中一個名稱拼寫錯誤,分析器將發出警告。

dart
import 'package:meta/meta.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> 完全相同。

換句話說,一個真正的包裝類可以封裝一個被包裝的物件,而擴充套件型別只是對被包裝物件的編譯時檢視。雖然真正的包裝器更安全,但權衡之下,擴充套件型別提供了避免包裝物件的選項,這在某些情況下可以顯著提高效能。