目次

拡張型

拡張型は、既存の型を異なる静的のみのインターフェースで「ラップ」するコンパイル時の抽象化です。これらは静的 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 であっても、クライアントが StringE を構築することだけを許可したい場合

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 句を使用して

  • 拡張型にサブタイプ関係を導入し、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

#

スーパタイプのメンバーと名前を共有する拡張型メンバーを宣言することは、クラス間のオーバーライド関係ではなく、むしろ*再宣言*です。拡張型メンバー宣言は、同じ名前のすべてのスーパタイプメンバーを*完全に置き換え*ます。同じ関数の代替実装を提供することはできません。

@redeclare アノテーションを使用すると、スーパタイプのメンバーと同じ名前を*意図的に*使用することを選択したことをコンパイラーに通知できます。アナライザーは、たとえば、名前の1つが誤って入力されているなど、それが実際には真実でない場合は警告します。

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. 既存の型に*拡張*インターフェースを提供する

#

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

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

これは、非透過的な拡張型とは異なり、表現型のメンバーを呼び出すことができるということです。例を以下に示します。

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;
}

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

もう1つの「ほぼ透過的」な拡張型のアプローチは、表現型のスーパークラスである型を実装することです。たとえば、表現型がプライベートであっても、そのスーパークラスがクライアントにとって重要なインターフェースの一部を定義している場合などです。

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

#

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

例えば、使用法で宣言した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'.
  ];
}

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

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

型の考慮事項

#

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

これは、拡張型が安全でない抽象化であることを意味します。なぜなら、実行時に常に表現型を特定し、基礎となるオブジェクトにアクセスできるからです。

動的な型テスト(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>とまったく同じものです。

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