擴充方法(Extension methods)

擴充方法能夠為現有型別、第三方函式庫型別新增功能,不必修改原始類別的程式碼。IDE 的程式碼自動補齊功能,會將擴充方法與原生方法一同列出提示。

你無法修改他人提供的 API,卻想額外新增方便使用的輔助方法。

需求範例:希望用 '42'.parseInt() 取代 int.parse('42')

語法架構

extension 擴充名稱? on 目標型別 {
  // 方法、取值器、設值器、運算子、靜態成員
}Code language: JavaScript (javascript)

範例

// 全部撰寫在同一支檔案,不需要拆分,main 可直接執行
extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }

  double parseDouble() {
    return double.parse(this);
  }
}

void main() {
  print('42'.parseInt());       // 42
  print('3.14'.parseDouble());  // 3.14
  print('100'.padLeft(6));      // 和原生 String 方法對照
}Code language: Dart (dart)

執行輸出

D:\dartdemo\firstdart>dart run
Building package executable...
Built firstdart:firstdart.
42
3.14
   100Code language: CSS (css)

擴充名稱為選填;this 代表呼叫此擴充功能的實體物件。

擴充功能支援的成員類型

擴充內可以定義:實體方法、取值器(getter)、設值器(setter)、運算子、靜態成員。

extension StringExt on String {
  // 取值器
  bool get isEmptyOrBlank => trim().isEmpty;

  // 實體方法
  String repeat(int count) => List.filled(count, this).join();

  // 運算子多載
  String operator *(int count) => repeat(count);

  // 靜態成員(需透過擴充名稱存取)
  static String emptyStr = '';
  static void printTip() => print("字串擴充靜態方法");
}

void main() {
  var str = " dart ";
  print(str.isEmptyOrBlank);
  print("hi" * 3);
  print("hello".repeat(2));

  // 存取靜態成員
  print(StringExt.emptyStr);
  StringExt.printTip();
}Code language: PHP (php)

執行結果

D:\dartdemo\firstdart>dart run
Building package executable...
Built firstdart:firstdart.
false
hihihi
hellohello

字串擴充靜態方法Code language: CSS (css)

未命名擴充(匿名擴充)

不填寫擴充名稱時,僅能在目前程式庫內存取,無法透過顯式呼叫解決名稱衝突。

無法在外部使用 擴充名稱(實體) 語法呼叫。

extension on String {
  bool get isBlank => trim().isEmpty;
}

void main() {
  print("   ".isBlank); // true
}Code language: Dart (dart)

匿名擴充裡的靜態成員,僅能在擴充內部使用。

泛型擴充(Generic extensions)

可以針對泛型型別建立擴充,例如 List<T>

extension MyFancyList<T> on List<T> {
  int get doubleLength => length * 2;
  List<T> operator -() => reversed.toList();
  List<List<T>> split(int at) => [sublist(0, at), sublist(at)];
}

void main() {
  List<int> list = [1, 2, 3, 4];
  print(list.doubleLength);          // 8
  print(-list);                      // [4,3,2,1]
  print(list.split(2));              // [[1,2],[3,4]]
}Code language: Dart (dart)

執行結果

D:\dartdemo\firstdart>dart run
Building package executable...
Built firstdart:firstdart.
8
[4, 3, 2, 1]
[[1, 2], [3, 4]]Code language: CSS (css)

static 靜態型別 vs dynamic

dynamic 變數無法呼叫擴充方法

擴充方法屬於靜態解析,執行階段才決定型別的 dynamic 變數找不到對應擴充:

void main() {
  dynamic d = '2';
  // print(d.parseInt()); // 執行階段發生 NoSuchMethodError

  var v = '2';
  print(v.parseInt()); // 型別推斷為 String,可以正常執行
}

extension NumberParsing on String {
  int parseInt() => int.parse(this);
}Code language: JavaScript (javascript)

原因:擴充方法在編譯時依照變數的靜態型別繫結,不是執行時動態查詢。

API 名稱衝突解決方案

多個擴充存在同名成員時,有三種處理方式。

1:匯入時透過 show / hide 隱藏衝突的擴充

// string_apis.dart 內含 NumberParsing
// string_apis_2.dart 內含 NumberParsing2,同樣定義 parseInt
import 'string_apis.dart';
import 'string_apis_2.dart' hide NumberParsing2;

void main() {
  print('42'.parseInt()); // 使用 string_apis.dart 的實作
}Code language: Dart (dart)

2:顯式呼叫擴充(包裝語法)

import 'string_apis.dart';    // NumberParsing
import 'string_apis_2.dart';  // NumberParsing2

void main() {
  // print('42'.parseInt()); // 產生衝突,編譯錯誤
  print(NumberParsing('42').parseInt());
  print(NumberParsing2('42').parseInt());
}Code language: Dart (dart)

3:匯入程式庫時使用前綴

兩個擴充名稱完全相同時,利用前綴區分:

import 'string_apis.dart';
import 'string_apis_3.dart' as rad;

void main() {
  print(NumberParsing('42').parseInt());
  print(rad.NumberParsing('42').parseInt());

  // 沒有衝突的擴充方法依舊可以隱式呼叫
  print('42'.parseNum());
}Code language: Dart (dart)

擴充方法(Extension methods)

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *