Dart 3.3のExtension typesはいいぞ
はじめに
Dart 3.3で導入したExtension types
はとても便利だなと思ってるので、公式ドキュメントに基づいてその特徴とユースケースを紹介します。
Extension typesの特徴
公式によると、Extension types
は既存の型を異なる、静的のみのインターフェイスでラップするもので、コンパイル時に型を決定します。
既存の型のインターフェースを簡単に変更でき、通常のラッパーを作成した際に発生するコストが発生しなくなるというメリットがあります。
extension type E(int i) {
// - コンストラクタが暗黙に宣言されるため、新しいコンストラクタは名前付きで宣言する必要がある
E.otherName(this.i);
// - 演算子や、Getter/Setter、メソッドなどを定義できる
E operator +(E other) => E(value + other.value);
E get myNum => this;
bool isValid() => !E.isNegative;
// - インスタンス変数と抽象メソッドは定義不可
String value = i.toString(); // Error
bool isNotValid(); // Error
}
ラッパークラスとの違い
extension type IdNumberA(int id) {
operator <(IdNumberA other) => id < other.id;
void printValue() {
print('Value is $id');
}
}
class IdNumberB {
IdNumberB(this.id);
final int id;
void printValue() {
print('Value is $id');
}
}
上の例では、Extension typesIdNumberA
とクラスIdNumberB
をそれぞれ作成しているのですが、相違点がいくつかあります。
-
IdNumberA
はDart標準のint
型としてコンパイルされるvoid main() { final safeIdA = IdNumberA(42); final unSafeIdA = safeIdA as int; // OK final safeIdB = IdNumberB(42); final unSafeIdB = safeIdB as int; // Error }
また、
IdNumberA
は基本型と互換性を持つものの、意味のある操作のみを定義することができます。void main() { final idA = IdNumberA(1); final idB = IdNumberA(2); print(idA < idB); // OK print(idA > idB); // Error }
-
Extension types
は完全に静的で、ランタイム時にコンパイルされるため、必要となるコストはほぼゼロ
従来のラッパークラス[1]と同じ役割を持っているが、ランタイムで余計なオブジェクトを生成することは不要になり、特に重いオブジェクトの場合にはコストを大幅に削減することが可能です。
Extension methodsとの違い
Extension methods
は似たような機能を持ちます。
ただ、Extension methods
は基になる型に対してメソッドなどを直接追加する一方、Extension types
は静的型(static type)である基本型を対象にする構文のみを追加できます。
Extension typesの使い道
以下のように、Extension types
は2つ見た目が全く同じだけど中身が全く違うユースケースがあります。
既存の型のインターフェイスを拡張する
implements
を利用して実装する場合、そのExtension types
は透過的(transparent)
と考えても良いでしょう。
なぜなら、implements
を通して基になる型を「丸見え」にしているからです。
extension type NumberT(int value) implements int {
// 'int'でないことを明示的に宣言する
NumberT get i => this;
}
void main () {
// All OK: 透過性によって`Extension type`を`int`として使える
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`のメンバーにアクセスできない
v2.i;
}
既存の型と異なるインターフェイスを提供する
implements
を使わない(不透明な)Extension types
は、基になる型と異なる、全く新しい型として扱われます。
これにより、基になる型にアサインできなく、内部のメンバーにもアクセスできません。
extension type NumberE(int value) {
NumberE operator +(NumberE other) => NumberE(value + other.value);
NumberE get myNum => this;
bool isValid() => !value.isNegative;
}
void testE() {
var num1 = NumberE(1);
int num2 = NumberE(2); // Error: 'NumberE'を'int'にアサインできない
num.isValid(); // OK: `NumberE`のメンバーを呼び出せる
num.isNegative(); // Error: 'NumberE'に'isNegative'が定義されていない
var sum1 = num1 + num1; // OK: 'NumberE'に'+'が定義されている
var diff1 = num1 - num1; // Error: 'NumberE'に'-'が定義されていない
var diff2 = num1.value - 2; // OK: 'value'は`int`型である
var sum2 = num1 + 2; // Error: 'NumberE'は`int`型ではない
List<NumberE> numbers = [
NumberE(1),
num1.next, // OK: 'next'の戻り値は'NumberE'である
1, // Error: 'int'を'NumberE'にアサインできない
];
}
利用する際の注意点
冒頭で述べたような、Extension types
はコンパイル時に型を決定するため、実行時には表示型(基になる型)
に扱われます。
すなわち、動的型チェック(e is T
)、型変換(e as T
)、および他のランタイム時処理(switch (e) ...
や if (e case ...)
など)は、すべて表示型として実行されます。
そのため、Extension types
はランタイム時でも意図せずに基になる型にアクセスすることができてしまい、厳密な型安全ではありません。
まとめ
Extension types
はパフォーマンスが高い、使いやすいなどのメリットがあり、高級版のExtension methods
とも言えるでしょう。
ラッピングオブジェクト(例えばDDDのドメインモデル)を作成するときによく使われると考えています。
ただ、ランタイムには基になる型と混淆しごちゃごちゃになる可能性があるため、扱う際に諸々注意を払う必要があるかなと思います。
-
オブジェクトとして扱いたい対象を、クラスとして定義したもの。 ↩︎
Discussion
軽い、の誤りではないでしょうか。
通常ラッパオブジェクトのサイズは同じ(リファレンスひとつ)なので、ラップされるオブジェクトが軽いほどオーバヘッドが相対的に重くなります。
特に
int
の場合は通常リファレンスを格納する領域(ポインタ)に直接格納するとう最適化を行うことが多いので、おそらくラッパオブジェクトの方が何倍も大きいことになります。もっとも、
extension type
の醍醐味はFFI等における他言語オブジェクトに対する透過的かつ概ね型安全なアクセスを提供することにあると思います。例えば、
package: web
のAPIを読むと、Javascriptオブジェクトに対するextension type
が多数あります。他言語オブジェクトそのものではありませんが、JSONをデコードした結果の
Map<String, dynamic>
にも同様のことが言えます。手前味噌ですが、Dart 3.3で導入したextension typeのユースケース - Qiitaを参照ください。
コメントありがとうございます!
直感的にわかりやすく伝えるため「重い」という表現を用いました。
また、
extension type
の醍醐味とWEB APIについては全くおっしゃる通りです。この記事は言語の特性そのものをメインテーマにしたく、あえて紹介しませんでした。補足していただきありがとうございます!