Dart SDK 2.7.1 時点での拡張メソッド(Extension methods)について情報をまとめてみた。
🤔 Dart の拡張メソッドとは
クラスやインターフェースの機能拡張(メソッドの追加)をするための言語機能。
Kotlin の拡張関数 と同じようなことができる。
Dart 2.7 で正式サポートされた。
機能実装の背景
拡張メソッドの実装以前では、インターフェースの機能拡張をするための方法として static 関数やラッパークラスが用いられてきた。
しかし、
-
static 関数では
()
のネストが深くなりやすくて読みづらいし IDE の補完も活かせない - ラッパークラスでは追加したいメソッドの他に、既存のメンバメソッドについて転送(Forwarding)する手間がかかる
という面倒がある。
これらに対して拡張メソッドは、
- 追加したいメソッドのみを実装すればいい
- 使用する際のコードはメソッドチェーンの形となるので読みやすい
- IDE の補完も働く
という楽をさせてくれる。たすかる。
使用例
拡張機能( extension
)が定義されたライブラリを import
、または同じファイル内で宣言して使う。
コンフリクトの回避を考えると、機能ごとにファイルを作成して import
する方法がよさそう。
// 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());
}
}
// 拡張機能のライブラリをインポート
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 など
- ⚠フィールド(メンバ変数)はダメ
💀ダメな使い方
上記の拡張機能 NumberParsing
を参考に。
💥 拡張先のインターフェースのメソッドを上書きする
-
常に拡張先のメソッドが優先され、同名の拡張メソッドは定義できない
-
👉 拡張メソッドは諦めてラッパークラスや
mixin
などで解決するextension BadExtension on Object { // BAD: 拡張先のメソッドと同名になっている String toString() => 'extension: $this'; }
💥 dynamic
型から呼び出す
-
拡張メソッドは静的に解決されるのでエラーとなる
(呼び出している式の型によってどの拡張メソッドを呼び出すかが解決される) -
👍 型キャストしたり、
dynamic
ではなくObject
を使ったりする// BAD: dynamic型から呼び出し dynamic x = '31'; x.show('bad'); // GOOD: 型キャストしてから呼び出し String y = x; y.show('cast');
💥 拡張機能 をオブジェクトとして扱う(インスタンス化する)
-
インスタンスを作成できそうな構文を書けるが実際に作成できたりはしない
-
拡張メソッドや元々のメンバを参照することのみが可能、オブジェクトとしては扱えない
- フィールドもコンストラクタも持てない
-
⚠ なお拡張メソッドについては関数オブジェクトとして扱える
-
👍 拡張機能は拡張メソッドおよびメンバを呼び出すためだけに使う
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
💣 常に 拡張機能を明示して使う
-
コード記述が増えるし読みやすさも下がる
-
🐈 コンフリクト解消のため明示的に使うのはヨシ!(後述)
-
👍 拡張先から
.
で拡張メソッドを呼び出す(暗黙的な利用)// AVOID: 明示的に拡張機能を指定しての呼び出し NumberParsing('42').show('avoid'); // GOOD: 暗黙的に拡張機能を利用して呼び出し '42'.show('good');
🍣その他
スコープの制限
通常は import
すればどこでもその拡張機能を使える。
宣言しているファイル中のみにスコープを制限する方法は次の 2 通り。
- 拡張機能名を省略して宣言する
- アンダースコア(
_
)で始まる名前を付ける
コンフリクト
複数の拡張機能を import
したときに同名の拡張メソッドが重複した場合、呼び出している式の型と on
句で指定されている型が最も近い拡張メソッドが優先される。
上記で解決できない、同じ型が指定されていた場合はコンフリクトする。
💡コンフリクトの解消方法
-
import
時にshow
句やhide
句を用いて利用する API を制限するstring_apis2.dartの拡張機能を隠すimport "string_apis.dart"; import "string_apis2.dart" hide NumberParsing2; void main() { // string_apis.dart の拡張メソッド print('42'.parseInt); }
-
ソース箇所によって使い分けたい場合
-
明示的に拡張機能を指定する
拡張機能を明示的に使用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); }
-
拡張機能まで同名の場合は
import
時にas
句で接頭辞を付ける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); }
-
🐶おわりに
拡張メソッドでいろいろできそうですね!
もうすでにライブラリが作られてる中で time とか dartx とか便利そう。
Dart 2.7 では他にもテクニカルプレビューながら Null Safety が準備され始めてるので、こっちの正式サポートも待ち遠しいです。