メインコンテンツにスキップ

拡張型

拡張型は、既存の型を異なる静的専用インターフェースで「ラップ」するコンパイル時抽象化です。これらは、実際のラッパーのコストを発生させることなく、既存の型のインターフェース(あらゆる種類の相互運用に不可欠)を簡単に変更できるため、静的JS相互運用の主要なコンポーネントです。

拡張型は、基になる型のオブジェクト(表現型と呼ばれる)に利用可能な操作(またはインターフェース)のセットに規律を課します。拡張型のインターフェースを定義する際に、表現型のメンバーの一部を再利用したり、一部を省略したり、一部を置き換えたり、新しい機能を追加したりできます。

次の例では、int型をラップして、ID番号に適した操作のみを許可する拡張型を作成します。

dart
extension type IdNumber(int id) {
  // Wraps the 'int' type's '<' operator:
  operator <(IdNumber other) => id < other.id;
  // Doesn't declare the '+' operator, for example,
  // because addition does not make sense for ID numbers.
}

void main() {
  // Without the discipline of an extension type,
  // 'int' exposes ID numbers to unsafe operations:
  int myUnsafeId = 42424242;
  myUnsafeId = myUnsafeId + 10; // This works, but shouldn't be allowed for IDs.

  var safeId = IdNumber(42424242);
  safeId + 10; // Compile-time error: No '+' operator.
  myUnsafeId = safeId; // Compile-time error: Wrong type.
  myUnsafeId = safeId as int; // OK: Run-time cast to representation type.
  safeId < IdNumber(42424241); // OK: Uses wrapped '<' operator.
}

構文

#

宣言

#

extension type宣言と名前、それに続く括弧内の表現型宣言を使用して、新しい拡張型を定義します。

dart
extension type E(int i) {
  // Define set of operations.
}

表現型宣言(int i)は、拡張型Eの基になる型がintであり、表現オブジェクトへの参照がiという名前であることを指定します。この宣言はまた、以下を導入します。

  • 表現オブジェクトの暗黙的なゲッター。戻り値の型は表現型です: int get i
  • 暗黙的なコンストラクタ: E(int i) : i = i

表現オブジェクトは、拡張型に基になる型のオブジェクトへのアクセスを提供します。オブジェクトは拡張型本体のスコープ内にあり、ゲッターとしてその名前を使用してアクセスできます。

  • 拡張型本体内ではi(またはコンストラクタ内のthis.i)を使用します。
  • 外部では、プロパティ抽出e.i(ここでeは拡張型を静的型として持ちます)を使用します。

拡張型宣言には、クラスや拡張機能と同様に、型パラメータを含めることもできます。

dart
extension type E<T>(List<T> elements) {
  // ...
}

コンストラクタ

#

拡張型の本体にコンストラクタをオプションで宣言できます。表現宣言自体は暗黙的なコンストラクタであるため、デフォルトで拡張型の名前のないコンストラクタの代わりになります。追加の非リダイレクト生成コンストラクタは、初期化リストまたは形式パラメータでthis.iを使用して表現オブジェクトのインスタンス変数を初期化する必要があります。

dart
extension type E(int i) {
  E.n(this.i);
  E.m(int j, String foo) : i = j + foo.length;
}

void main() {
  E(4); // Implicit unnamed constructor.
  E.n(3); // Named constructor.
  E.m(5, "Hello!"); // Named constructor with additional parameters.
}

または、表現宣言コンストラクタに名前を付けることもできます。この場合、本体に名前のないコンストラクタのためのスペースがあります。

dart
extension type const E._(int it) {
  E(): this._(42);
  E.otherName(this.it);
}

void main2() {
  E();
  const E._(2);
  E.otherName(3);
}

クラスのプライベートコンストラクタ構文_と同じものを使用して、コンストラクタを定義するのではなく、完全に非表示にすることもできます。たとえば、基になる型がintであっても、クライアントがEStringで構築することを望む場合。

dart
extension type E._(int i) {
  E.fromString(String foo) : i = int.parse(foo);
}

フォワード生成コンストラクタ、またはファクトリコンストラクタ(サブ拡張型のコンストラクタにフォワードすることもできます)を宣言することもできます。

メンバー

#

拡張型の本体にメンバーを宣言して、クラスメンバーと同様の方法でインターフェースを定義します。拡張型メンバーは、メソッド、ゲッター、セッター、または演算子にできます(externalでないインスタンス変数および抽象メンバーは許可されません)。

dart
extension type NumberE(int value) {
  // Operator:
  NumberE operator +(NumberE other) =>
      NumberE(value + other.value);
  // Getter:
  NumberE get myNum => this;
  // Method:
  bool isValid() => !value.isNegative;
}

表現型のインターフェースメンバーは、デフォルトでは拡張型のインターフェースメンバーではありません(こちらを参照)。表現型の単一メンバーを拡張型で利用可能にするには、拡張型定義で宣言を記述する必要があります。これはNumberEoperator +のようなものです。また、表現型に関連しない新しいメンバーを定義することもできます。これは、iゲッターやisValidメソッドのようなものです。

implements

#

オプションでimplements句を使用して、以下を行うことができます。

  • 拡張型にサブタイプ関係を導入する、AND
  • 表現オブジェクトのメンバーを拡張型インターフェースに追加する。

implements句は、拡張メソッドとそのon型との間の適格性関係を導入します。超型の適用可能なメンバーは、サブタイプに同じメンバー名を持つ宣言がある場合を除き、サブタイプにも適用可能です。

拡張型は以下のみを実装できます。

  • その表現型。これにより、表現型のすべてのメンバーが拡張型で暗黙的に利用可能になります。

    dart
    extension type NumberI(int i) 
      implements int{
      // 'NumberI' can invoke all members of 'int',
      // plus anything else it declares here.
    }
  • その表現型の超型。これにより、超型のメンバーが利用可能になりますが、必ずしも表現型のすべてのメンバーが利用可能になるわけではありません。

    dart
    extension type Sequence<T>(List<T> _) implements Iterable<T> {
      // Better operations than List.
    }
    
    extension type Id(int _id) implements Object {
      // Makes the extension type non-nullable.
      static Id? tryParse(String source) => int.tryParse(source) as Id?;
    }
  • 同じ表現型で有効な別の拡張型。これにより、複数の拡張型間で操作を再利用できます(多重継承に似ています)。

    dart
    extension type const Opt<T>._(({T value})? _) { 
      const factory Opt(T value) = Val<T>;
      const factory Opt.none() = Non<T>;
    }
    extension type const Val<T>._(({T value}) _) implements Opt<T> { 
      const Val(T value) : this._((value: value));
      T get value => _.value;
    }
    extension type const Non<T>._(Null _) implements Opt<Never> {
      const Non() : this._(null);
    }

implementsのさまざまなシナリオでの影響については、使用法セクションをお読みください。

@redeclare

#

超型のメンバーと同じ名前を持つ拡張型メンバーを宣言することは、クラス間のオーバーライド関係ではなく再宣言です。拡張型メンバー宣言は、同じ名前を持つ超型メンバーを完全に置き換えます。同じ関数に代替実装を提供することはできません。

package:meta@redeclareアノテーションを使用して、コンパイラに超型のメンバーと同じ名前を意図的に選択していることを伝えることができます。アナライザは、たとえば名前の1つがタイプミスである場合、それが実際にはそうでない場合に警告を表示します。

dart
import 'package:meta/meta.dart';

extension type MyString(String _) implements String {
  // Replaces 'String.operator[]'.
  @redeclare
  int operator [](int index) => codeUnitAt(index);
}

また、annotate_redeclaresリントを有効にして、超インターフェースメンバーを隠す拡張型メソッドを宣言し、@redeclareでアノテーションが付けられていない場合に警告を表示することもできます。

使用法

#

拡張型を使用するには、クラスを使用する場合と同じように、コンストラクタを呼び出してインスタンスを作成します。

dart
extension type NumberE(int value) {
  NumberE operator +(NumberE other) =>
      NumberE(value + other.value);

  NumberE get next => NumberE(value + 1);
  bool isValid() => !value.isNegative;
}

void testE() { 
  var num = NumberE(1);
}

次に、クラスオブジェクトの場合と同様に、オブジェクトのメンバーを呼び出すことができます。

拡張型のコアユースケースには、同等に有効ですが、実質的に異なる2つのケースがあります。

  1. 既存の型に拡張されたインターフェースを提供する。
  2. 既存の型に異なるインターフェースを提供する。

1. 既存の型に拡張されたインターフェースを提供する

#

拡張型がその表現型をimplementsすると、拡張型が基になる型を「見ることができる」ため、「透過的」と見なすことができます。

透過的な拡張型は、再宣言されたメンバー(redeclare)および定義した補助メンバーに加えて、表現型のすべてのメンバーを呼び出すことができます。これにより、既存の型の新しい、拡張されたインターフェースが作成されます。新しいインターフェースは、静的型が拡張型である式で利用できます。

これは、非透過的拡張型とは異なり)表現型のメンバーを呼び出すことができることを意味します。たとえば、次のようになります。

dart
extension type NumberT(int value) 
  implements int {
  // Doesn't explicitly declare any members of 'int'.
  NumberT get i => this;
}

void main () {
  // All OK: Transparency allows invoking `int` members on the extension type:
  var v1 = NumberT(1); // v1 type: NumberT
  int v2 = NumberT(2); // v2 type: int
  var v3 = v1.i - v1;  // v3 type: int
  var v4 = v2 + v1; // v4 type: int
  var v5 = 2 + v1; // v5 type: int
  // Error: Extension type interface is not available to representation type
  v2.i;
}

また、新しいメンバーを追加したり、スーパータイプから指定されたメンバー名を再宣言したりして他のメンバーを適応させたりする「ほぼ透過的」な拡張型を持つこともできます。これにより、たとえば、メソッドのいくつかのパラメータに厳密な型を使用したり、異なるデフォルト値を使用したりできます。

別のほぼ透過的な拡張型の方法は、表現型の超型である型を実装することです。たとえば、表現型がプライベートであるが、その超型がクライアントにとって重要なインターフェース部分を定義している場合。

2. 既存の型に異なるインターフェースを提供する

#

透過的でない)(implementしない)拡張型は、表現型とは異なる完全に新しい型として静的に扱われます。それを表現型に割り当てることはできず、表現型のメンバーを公開しません。

たとえば、使用法の下で宣言したNumberE拡張型を使用してみましょう。

dart
void testE() { 
  var num1 = NumberE(1);
  int num2 = NumberE(2); // Error: Can't assign 'NumberE' to 'int'.
  
  num1.isValid(); // OK: Extension member invocation.
  num1.isNegative(); // Error: 'NumberE' does not define 'int' member 'isNegative'.
  
  var sum1 = num1 + num1; // OK: 'NumberE' defines '+'.
  var diff1 = num1 - num1; // Error: 'NumberE' does not define 'int' member '-'.
  var diff2 = num1.value - 2; // OK: Can access representation object with reference.
  var sum2 = num1 + 2; // Error: Can't assign 'int' to parameter type 'NumberE'. 
  
  List<NumberE> numbers = [
    NumberE(1), 
    num1.next, // OK: 'next' getter returns type 'NumberE'.
    1, // Error: Can't assign 'int' element to list type 'NumberE'.
  ];
}

この方法で拡張型を使用して、既存の型のインターフェースを置き換えることができます。これにより、新しい型の制約に適したインターフェース(導入部のIdNumber例のような)をモデル化しながら、intのような単純な事前定義された型のパフォーマンスと利便性から恩恵を受けることができます。

このユースケースは、ラッパー クラスの完全なカプセル化に最も近いものですが、実際にはある程度保護された抽象化にすぎません。

型の考慮事項

#

拡張型はコンパイル時ラッパー構造です。実行時には、拡張型の痕跡はまったくありません。型クエリや同様の実行時操作は、表現型に対して機能します。

これは拡張型を安全でない抽象化にします。なぜなら、実行時に常に表現型を見つけ出し、基になるオブジェクトにアクセスできるからです。

動的型テスト(e is T)、キャスト(e as T)、その他の実行時型クエリ(switch (e) ...またはif (e case ...)など)はすべて基になる表現オブジェクトに評価され、そのオブジェクトの実行時型に対して型チェックされます。これは、eの静的型が拡張型である場合、および拡張型(case MyExtensionType(): ...)に対してテストする場合に当てはまります。

dart
void main() {
  var n = NumberE(1);

  // Run-time type of 'n' is representation type 'int'.
  if (n is int) print(n.value); // Prints 1.

  // Can use 'int' methods on 'n' at run time.
  if (n case int x) print(x.toRadixString(10)); // Prints 1.
  switch (n) {
    case int(:var isEven): print("$n (${isEven ? "even" : "odd"})"); // Prints 1 (odd).
  }
}

同様に、一致した値の静的型は、この例では拡張型の型になります。

dart
void main() {
  int i = 2;
  if (i is NumberE) print("It is"); // Prints 'It is'.
  if (i case NumberE v) print("value: ${v.value}"); // Prints 'value: 2'.
  switch (i) {
    case NumberE(:var value): print("value: $value"); // Prints 'value: 2'.
  }
}

拡張型を使用する際には、この品質を認識しておくことが重要です。拡張型はコンパイル時に存在し、重要ですが、コンパイル中に消去されることを常に念頭に置いてください。

たとえば、静的型が拡張型Eで、Eの表現型がRである式eを考えてみましょう。この場合、eの値の実行時型はRのサブタイプです。型自体も消去されます。実行時には、List<E>List<R>とまったく同じです。

言い換えれば、実際のラッパー クラスはラップされたオブジェクトをカプセル化できますが、拡張型はラップされたオブジェクトのコンパイル時ビューにすぎません。実際のラッパーはより安全ですが、トレードオフとして、拡張型はラッパーオブジェクトを回避するオプションを提供し、一部のシナリオではパフォーマンスを大幅に向上させることができます。