2020/8/26 Dart : Extension methodsの訳

Extension methods

Dart2.7で導入されたExtension methodは、既存のライブラリに新たな機能を付け加える方法です。あなたは知らず知らずのうちにextension methodを使っているかもしれません。例えば、IDEでコード補完を使用すると、通常のメソッドに加えて拡張メソッドが提案されます。

Overview(概要)

誰かが作ったAPIを使う時、あるいはあなたが幅広く使われるライブラリを実装する時、APIを変更することが不可能、あるいは現実的では無いケースはよくあります。

例えば、文字列を数値へと解析(変換)する以下のコードを考えてみます。

int.parse('42')

↑int型の静的メソッド(クラスメソッド)としてparseメソッドが用意されている。parseメソッドはString型を引数にとる、ということ。

上記の型式よりも、String型のインスタンスメソッドとして「そのString型インスタンスを数値に変換して返すparseIntメソッド」を用意した方がよりniceかもしれません。つまり↓

'42'.parseInt()

ということですね。しかし実際にはStringクラスにparseInt()という名の、自身を数値に変換するメソッドは定義されていません。よってDartpadで以下のコードを実行すると下記のようなエラーが出ます。

void main(){
  print("${"45".parseInt()}");
}

/*
Error compiling to JavaScript:
main.dart:13:17:
Error: The method 'parseInt' isn't defined for the class 'String'.
  print("${"45".parseInt()}");
                ^^^^^^^^
Error: Compilation failed.
*/

Stringクラスに対して上記のparseIntメソッド(の機能)を付け加えたい場合に、Stringクラスの定義内にparseIntメソッドの定義を実装するのではなく、

下記のように書くことで機能を追加することができます。この書き方がextension。

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
  // ···
}

void main(){
  print("${"45".parseInt()}");
}

/*
45
*/

 

Extension(拡張)はメソッドだけでなく、ゲッター、セッターや演算子などの他のメンバを定義することもできます。またextensionには名前を拡張することもできます。これはAPIの名前衝突の発生を避けるのに使えます。(NumberParsingという名のextensionを使った)extension methodのparseInt()の実装の仕方が上記です。

次のセクションでは、extension methodの使い方を説明します。その後がextension methodの実装の方法のセクションです。


Using extension methods

全てのDartのコードのように、extension methodはライブラリにあります。すでにextension methodの使い方を見ましたが、(extension methodがあるライブラリを)import(インポート)して、通常のメソッドのように使うだけです。

//↓Stringクラスへのextension(拡張)を含んだライブラリをインポートする
import 'string_apis.dart';
// ···
print('42'.padLeft(5)); // Use a String method.
print('42'.parseInt()); // Use an extension method.

これがextension methodを使用するために知っておくべきことの全てです。

コードを記述するときは、拡張メソッドが静的型に依存する方法(dynamicとは対照的)とAPIの競合を解決する方法も知っておく必要があります。

Static types and dynamic

dynamic型の変数でextension methodを実行することはできません。例えば、以下のコードは実行時例外を発生させます。

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
  // ···
}

void main(){
  dynamic d = '2';
  print(d.parseInt()); // Runtime exception:     NoSuchMethodError
}

Extension methodはDartの型推論と共に機能します。下記のコードは、変数vがString型であると型推論されますので、問題ありません。

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
  // ···
}

void main(){
  var v = '2';
  print(v.parseInt()); // Output: 2
}

dynamic型に対してextension methodを呼び出せない理由は、エクステンションメソッドがレシーバーの静的タイプ(static type)に対して解決されるためです。拡張メソッドは静的に解決されるため、静的関数を呼び出すのと同じくらい高速です。


API conflicts

extension methodの名前が、インターフェース(拡張対象の型のメンバメソッド)の名前と衝突する、あるいは別のextension methodの名前と衝突する場合、いくつかの対処法があります。

一つの方法は、show,hideキーワードを使ってインポートするAPIを制限する方法です。

// lib/extensions/string_apis.dart

extension NumberParsingPlus1 on String {
  int parseInt() {
    return int.parse(this)+1;
  }
// ···
}

extension NumberParsingPlus100 on String {
  int parseInt() {
    return int.parse(this)+100;
  }
// ···
}
// lib/extensions/string_apis_2.dart

extension NumberParsingMinus1 on String {
  int parseInt() {
    return int.parse(this)-1;
  }
// ···
}

extension NumberParsingMinus100 on String {
  int parseInt() {
    return int.parse(this)-100;
  }
// ···
}
// lib/main.dart
import 'package:flu_1015/extentions/string_apis.dart' show NumberParsingPlus1;
import 'package:flu_1015/extentions/string_apis_2.dart' hide NumberParsingMinus1,NumberParsingMinus100;

main() {
  print('4323'.parseInt()); // Use an extension method.
}
/*
flutter: 4324
*/

showキーワードはファイルの中の指定したエクステンション名のエクステンションのみインポートする。上記サンプルでは

string_apis.dartのNumberParsingPlus1のみインポートする。

hideキーワードはファイルの中の指定したエクステンション名のエクステンションを隠す。上記サンプルでは

string_apis_2.dartのNumberParsingMinus1,NumberParsingMinus100を隠す。

(結果的にstring_apis_2.dartをインポートしないのと同じ状態)

ということで、NumberParsingPlus1のみがインポートされるので、

4323+1=4324

が表示されている。


もう一つの方法は、明示的にエクステンション名を指定する方法です。これにより、拡張機能がラッパークラスであるかのように見えるコードが生成されます。

// lib/main.dart
//string_apis.dart,string_apis_2.dartは一つ前のサンプルと同じ。
main() {
  print(NumberParsingPlus1('4323').parseInt()); //4324
  print(NumberParsingPlus100('4323').parseInt()); //4423
  print(NumberParsingMinus1('4323').parseInt()); //4322
  print(NumberParsingMinus100('4323').parseInt()); //4223
}

 

 


もう一つの方法は、extensionを明示的に適用する方法です。この書き方はextensionがラッパークラスのように見える方法です。

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
  // ···
}

extension NumberParsing_2 on String{
  int parseInt(){
    return int.parse(this)+2;
  }
}

void main(){
  var v = '2';
  print(NumberParsing(v).parseInt());
  print(NumberParsing_2(v).parseInt());
}

/*
2
4
*/

もし同じ名前のextensionが二つある場合、下記のようにprefixを使ってインポートする必要があります。

// string_apis.dartとstring_apis_3.dartの両方が
// NumberParsingという同じ名前のextensionを持っていて、
// その中にparseInt()という同じ名前のextension methodを持っている場合。
// string_apis_3.dartにはparseNum()というextension methodもある。
import 'string_apis.dart';
import 'string_apis_3.dart' as rad;

// ···
// print('42'.parseInt()); // Doesn't work.

// string_apis.dart.のparseInt()が呼び出される。
print(NumberParsing('42').parseInt());

// string_apis_3.dartのparseInt()が呼び出される。
print(rad.NumberParsing('42').parseInt());

// string_apis_3.dart だけが parseNum()メソッドを持っている場合。
//名前が被ってないので以下の書き方でstring_apis.dartのparseNum()が呼び出される。
print('42'.parseNum());

上記の例の最後の行が示すように、prefix(上記サンプルではrad)を使ってimportした場合でも、extension methodを暗黙的に実行することはできます。prefixを使用する必要があるのは、extension名を明示的に使った時に名前衝突を避ける時だけです。

↑柔軟性があると言えるか。ただ、知らないと混乱しそうな気もする。


Implementing extension methods(extension methodの実装)

extension methodを定義する場合以下のように記述します。

extension <extension name> on <type> {
  (<member definition>)*
}

例えば、Stringクラスに対してextensionを実装する場合、

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }

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

宣言されたライブラリ内でのみ利用可能なローカルextensionを生成したい場合、extension名を省略するか、アンダースコア( _ )から始まる名前のextensionを用意するかのどちらかです。

extensionのメンバはメソッド・ゲッター・セッター・演算子です。Extensionはstaticフィールドとstaticヘルパーメソッドを持つこともできます。


Implementing generic extensions

Extensionはジェネリック型の型引数を持つことができます。例えば、組み込み型のList<T>型に対してゲッター・演算子・メソッドを拡張した例です。

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

 

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


main() {
  List<int> list1=[1,2,3,4,5];
  print(list1.doubleLength); //10
  print(-list1); //[5, 4, 3, 2, 1]
  print(list1.split(2)); //[[1, 2], [3, 4, 5]]
}

工事中🏗


Resources

For more information about extension methods, see the following:

 

参考

https://dart.dev/guides/language/extension-methods

 

“2020/8/26 Dart : Extension methodsの訳” への1件の返信

コメントを残す

メールアドレスが公開されることはありません。