16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Dartの拡張メソッド(Extension methods)について

Posted at

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

🤔 Dart の拡張メソッドとは

クラスやインターフェースの機能拡張(メソッドの追加)をするための言語機能。
Kotlin の拡張関数 と同じようなことができる。
Dart 2.7 で正式サポートされた。

機能実装の背景

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

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

という面倒がある。

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

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

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

使用例

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

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());
  }
}
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 を使わずともメンバであるかのように参照可能
  • ジェネリクスも使える
  • 拡張メソッドとして定義できるのはメソッド、演算子、settergetter など
    • ⚠フィールド(メンバ変数)はダメ

💀ダメな使い方

上記の拡張機能 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 句で指定されている型が最も近い拡張メソッドが優先される。

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

💡コンフリクトの解消方法

  1. 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);
    }
    
  2. ソース箇所によって使い分けたい場合

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

      拡張機能を明示的に使用
      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);
      }
      
    2. 拡張機能まで同名の場合は 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 が準備され始めてるので、こっちの正式サポートも待ち遠しいです。

参考

16
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?