Dartの拡張メソッド（Extension methods）について

Dart SDK 2.7.1 時点での拡張メソッド（Extension methods）について情報をまとめてみた。

## &#x1F914; Dart の拡張メソッドとは

クラスやインターフェースの機能拡張（メソッドの追加）をするための言語機能。
[_Kotlin_ の拡張関数](https://dogwood008.github.io/kotlin-web-site-ja/docs/reference/extensions.html) と同じようなことができる。
Dart 2.7 で正式サポートされた。

### 機能実装の背景

拡張メソッドの実装以前では、インターフェースの機能拡張をするための方法として _static_ 関数やラッパークラスが用いられてきた。
しかし、

- _static_ 関数では `()` のネストが深くなりやすくて読みづらいし IDE の補完も活かせない
- ラッパークラスでは追加したいメソッドの他に、既存のメンバメソッドについて転送（_Forwarding_）する手間がかかる

という面倒がある。

これらに対して拡張メソッドは、

- 追加したいメソッドのみを実装すればいい
- 使用する際のコードはメソッドチェーンの形となるので読みやすい
- IDE の補完も働く

という楽をさせてくれる。たすかる。

### 使用例

拡張機能（ `extension` ）が定義されたライブラリを `import` 、または同じファイル内で宣言して使う。
コンフリクトの回避を考えると、機能ごとにファイルを作成して `import` する方法がよさそう。

```dart:string_apis.dart
// String型を拡張する、拡張機能 'NumberParsing'
extension NumberParsing on String {

  // 呼び出し側の String オブジェクトを int型へ変換する拡張メソッド
  int get parseInt => int.parse(this);

  // 呼び出し側の String オブジェクトを接頭辞付きで表示する拡張メソッド
  void show([String prefix = '']) {
    final buf = StringBuffer(prefix.isEmpty ? '' : '$prefix: ')..write(this);
    print(buf.toString());
  }
}
```

```dart:main.dart
// 拡張機能のライブラリをインポート
import 'string_apis.dart';

void main() {
  final str = '42';

  // 拡張メソッド parseInt, show が利用できる
  print(str.parseInt); // => 42
  print(str.parseInt.runtimeType); // => int
  str.show('sample'); // => sample: 42

  // String 本来のメンバメソッドも使えるまま
  print(str.substring(1)); // => 2
}
```

### 特徴

- 簡単にインターフェースのメソッドを追加（したような振る舞いが）できる
- 宣言はトップレベルで行う
- 拡張メソッドの実体はただの _static_ 関数
- 呼び出し側のオブジェクトは `this` 句を使うことで参照可能
- 拡張先のメソッドは `this` を使わずともメンバであるかのように参照可能
- ジェネリクスも使える
- 拡張メソッドとして定義できるのはメソッド、演算子、_setter_、_getter_ など
  - &#x26A0;フィールド（メンバ変数）はダメ

## &#x1F480;ダメな使い方

上記の拡張機能 [`NumberParsing`](#使用例) を参考に。

&#x1F4A5; 拡張先のインターフェースのメソッドを上書きする

- 常に拡張先のメソッドが優先され、同名の拡張メソッドは定義できない
- &#x1F449; 拡張メソッドは諦めてラッパークラスや `mixin` などで解決する

  ```dart
  extension BadExtension on Object {
    // BAD: 拡張先のメソッドと同名になっている
    String toString() => 'extension: $this';
  }
  ```

---

&#x1F4A5; `dynamic` 型から呼び出す

- 拡張メソッドは静的に解決されるのでエラーとなる  
  （呼び出している式の型によってどの拡張メソッドを呼び出すかが解決される）
- &#x1F44D; 型キャストしたり、`dynamic` ではなく `Object` を使ったりする

  ```dart
  // BAD: dynamic型から呼び出し
  dynamic x = '31';
  x.show('bad');

  // GOOD: 型キャストしてから呼び出し
  String y = x;
  y.show('cast');
  ```

---

&#x1F4A5; **拡張機能** をオブジェクトとして扱う（インスタンス化する）

- インスタンスを作成できそうな構文を書けるが実際に作成できたりはしない
- 拡張メソッドや元々のメンバを参照することのみが可能、オブジェクトとしては扱えない
  - フィールドもコンストラクタも持てない
- &#x26A0; なお拡張メソッドについては関数オブジェクトとして扱える
- &#x1F44D; 拡張機能は拡張メソッドおよびメンバを呼び出すためだけに使う

  ```dart
  String str = '42';

  // BAD: 拡張機能をオブジェクトとして扱う
  var x = NumberParsing(str);

  // OK: 拡張メソッドをオブジェクトとして扱う
  var f = str.show; // void Function([String]) f

  // ただし同一オブジェクトにはならないことに注意
  var g = str.show;
  print(f != g); // => true
  ```

---

&#x1F4A3; **常に** 拡張機能を明示して使う

- コード記述が増えるし読みやすさも下がる
- &#x1F408; コンフリクト解消のため明示的に使うのはヨシ！（[後述](#コンフリクト)）
- &#x1F44D; 拡張先から `.` で拡張メソッドを呼び出す（暗黙的な利用）

  ```dart
  // AVOID: 明示的に拡張機能を指定しての呼び出し
  NumberParsing('42').show('avoid');

  // GOOD: 暗黙的に拡張機能を利用して呼び出し
  '42'.show('good');
  ```

## &#x1F363;その他

### スコープの制限

通常は `import` すればどこでもその拡張機能を使える。

宣言しているファイル中のみにスコープを制限する方法は次の 2 通り。

- 拡張機能名を省略して宣言する
- アンダースコア（`_`）で始まる名前を付ける

### コンフリクト

複数の拡張機能を `import` したときに同名の拡張メソッドが重複した場合、呼び出している式の型と `on` 句で指定されている型が最も近い拡張メソッドが優先される。

上記で解決できない、同じ型が指定されていた場合はコンフリクトする。

&#x1F4A1;コンフリクトの解消方法

1. `import` 時に `show` 句や `hide` 句を用いて利用する API を制限する

     ```dart:string_apis2.dartの拡張機能を隠す
     import "string_apis.dart";
     import "string_apis2.dart" hide NumberParsing2;

     void main() {
       // string_apis.dart の拡張メソッド
       print('42'.parseInt);
     }
     ```

1. ソース箇所によって使い分けたい場合

   1. 明示的に拡張機能を指定する

        ```dart:拡張機能を明示的に使用
        import "string_apis.dart";
        import "string_apis2.dart";

        void main() {
          // string_apis.dart の拡張メソッド
          print(NumberParsing('42').parseInt);

          // string_apis2.dart の拡張メソッド
          print(NumberParsing2('42').parseInt);

          // コンフリクトエラー
          // print('42'.parseInt);
        }
        ```

   1. 拡張機能まで同名の場合は `import` 時に `as` 句で接頭辞を付ける

        ```dart:string_apis3.dartに接頭辞を付けて区別
        import "string_apis.dart";
        import "string_apis3.dart" as rad;

        void main() {
          // string_apis.dart の拡張メソッド
          print(NumberParsing('42').parseInt);

          // string_apis3.dart の拡張メソッド
          print(rad.NumberParsing('42').parseInt);
        }
        ```

## &#x1F436;おわりに

拡張メソッドでいろいろできそうですね！
もうすでにライブラリが作られてる中で [time](https://pub.dev/packages/time) とか [dartx](https://pub.dev/packages/dartx) とか便利そう。

Dart 2.7 では他にもテクニカルプレビューながら _Null Safety_ が準備され始めてるので、こっちの正式サポートも待ち遠しいです。

## 参考

- [Extension methods](https://dart.dev/guides/language/extension-methods)
- [Announcing Dart 2.7: A safer, more expressive Dart][announce]
- [Dart extension method fundamentals][article]
- [Feature specification -- dart-lang/language](https://github.com/dart-lang/language/blob/master/accepted/2.6/static-extension-members/feature-specification.md#dart-static-extension-methods-design)

[article]: https://medium.com/dartlang/extension-methods-2d466cd8b308
[announce]: https://medium.com/dartlang/dart-2-7-a3710ec54e97