Dart 3.3で導入したextension typeの解説

# はじめに
Dart 3.3で導入した`extension type`は既存の型のメモリ表現を借りたクラス風の型です。
メモリ表現を与える`final`なフィールドを一つだけ持ちます。
そのフィールドのラッパーオブジェクトを持たないので効率的です。
[クラスにも広く導入予定のプライマリコンストラクタ](https://github.com/dart-lang/language/blob/main/working/2364%20-%20primary%20constructors/feature-specification.md "language/working/2364 - primary constructors/feature-specification.md at main · dart-lang/language")記法で定義します。

Kotlinを知っている人には[value class](https://kotlinlang.org/docs/inline-classes.html "Inline classes | Kotlin Documentation")と説明するのが理解への近道かと思います。

ここでは、クラスと比較した特異性を中心に解説します。

# 想定する使い方
自然数(1以上の整数)に限定の`Id`型を想定します。

```dart
extension type Id._(int value) {
  Id(this.value): assert(value >= 1);
  bool get isEven => value.isOdd; // lying
  void show() => print('[$value]');
}

void main() {
  Id id = Id(123); // 推奨形は ver id = Id(123);
  int i = id.value; // 推奨形は var i = id.value;
  id.show(); // [123]
  print(id.isEven); // true (deceived)

  id = Id(-1); // runtime AssertionError

  i = id; // compile-time error
  id = i; // compile-time error
}
```

[プライマリコンストラクタ記法では`assert()`を指定できません](https://github.com/dart-lang/language/issues/3354 "Extension type should be able to have assert for the parameter of the constructor · Issue #3354 · dart-lang/language")。([参考その2](https://github.com/dart-lang/language/blob/ed8f9402e467b6ef522d3851341484eb5ea8d668/working/2364%20-%20primary%20constructors/feature-specification.md?plain=1#L89C1-L91C28 "language/working/2364 - primary constructors/feature-specification.md at ed8f9402e467b6ef522d3851341484eb5ea8d668 · dart-lang/language"))
ここでは`assert`を使いたかったので無名コンストラクタを型(≒クラス)ボディ内コンストラクタとし、プライマリコンストラクタを(プライベートな)名前付きコンストラクタにしました。

# 特異性
### プライマリコンストラクタ記法が必須
`extension type`は`final`なフィールドを一つだけ持つ無名(位置)引数のプライマリコンストラクタ記法で定義する必要があります。
厳密には[extension typeコンストラクタ](https://github.com/dart-lang/language/blob/main/accepted/future-releases/extension-types/feature-specification.md#extension-type-constructors-and-their-static-analysis "language/accepted/future-releases/extension-types/feature-specification.md at main · dart-lang/language")であり、`final`は省略します。
裏を返すと、フィールドは`final`であることが強制されます。
その無名引数が`final`フィールドの定義と初期化子を兼ねるプライマリコンストラクタ記法を強制することによって、唯一のフィールドがメモリ表現を提供することを示唆する狙いがあるものと思われます。

なお、先の例のように型ボディ内でも別途コンストラクタを定義できます。

### 明示的キャスト
`extension type`とそのフィールド型は互いに明示的キャストできます。

後述の動的な挙動を静的にも記述可能とするために必要な措置なのだと思いますが、継承関係にないものどうしが明示的キャストできることに違和感が有ります。
また、明示的ダウンキャストではその正当性が実行時にその場でチェックされるのに対し、`extension type`への明示的キャストではユーザがコンストラクタに定義したチェックロジックをすり抜けるという意味でやはり特異です。
~~いやむしろ、`assert`を指定できなかったextension typeコンストラクタが呼ばれる、と理解すべきでしょう。~~

```dart
  i = id as int; // ok!
  i = -1;
  id = i as Id; // ok!!
  print(id); // -1 !?
```

従って、[新たなlint](https://github.com/dart-lang/linter/issues/4515#issuecomment-1614441489 "Lint for cast to inline class · Issue #4515 · dart-lang/linter")の導入が検討されています。
なお、~~はたまた、~~[extension typeコンストラクタに`assert`が指定でき](https://github.com/dart-lang/language/issues/3343#issuecomment-1719782233 "Awkward two-constructor necessity in extension type with validation · Issue #3343 · dart-lang/language")~~るようになるのかもしれません~~たとしてもextension typeコンストラクタが呼ばれるわけではないようです。

### `Object?`の５メンバ
トップ`Object?`と共通の`runtimeType`、`hashCode`、`operator==`、`toString`、`noSuchMethod`の５メンバはフィールドのそれらのコピーとして暗黙的に宣言・定義されます。
また、ユーザは`Object?`の５メンバを再宣言・定義(≒オーバライド)することはできません。

なお、`runtimeType`が返すべき`extension type`用の`Type`型の値も存在しません。

### dynamic変数等を経由したアクセス
`extension type`は完全に静的な型であり、`dynamic`変数経由のアクセスにおける実行時の振る舞いはフィールド型のそれになります。
従って、`extension type`で独自に宣言・定義したメンバ`show()`等を参照すると実行時エラーです。
反対に、`extension type`で独自に宣言・定義せず、フィールド型で宣言・定義された`isOdd`などが呼べてしまいます。

```dart
  dynamic d = id;
  d.show(); // runtime NoSuchMethodError
  d = d + 1; // ok
  print(d.isOdd); // false
```

先の`isEven`のように既にフィールド型で宣言・定義され、`extension type`で再宣言・定義したメンバは、[コンテクストによって動作が異なることになるため、混乱のもとです](https://github.com/dart-lang/language/issues/3168 "Should inline class members with same names as those of the field be targets of lint? · Issue #3168 · dart-lang/language")。
```dart
  d = id;
  print(identical(id, d)); // true
  print(id.isEven); // true (deceived)
  print(d.isEven); // false (confusing)
```

同様に、`Object?`等のスーパータイプ変数を経由したアクセスも要注意です。
`runtimeType`による判断(`is`等)のもと型固有のメンバにアクセスしようにも、`extension type`には固有の`runtimeType`が存在しませんので。

ちなみに、先述の`Object?`の5メンバの再宣言・定義が(とりあえず)禁止されているのも、同様の混乱を避けるのが目的です。
仮に、`toString`が再宣言・定義できた場合、`print(i64)`と`print(i64.toString())`で異なる結果となる、厄介な問題が発生します。
前者は`print`内で動的に`int`の`toString`に解決し、後者は`print`のコールサイトで静的に`I64`の`toString`に解決します。
仮に、`hashCode`と`operator==`が再宣言・定義出来た場合はもっと深刻かもしれません。

### フィールド型のインタフェースの実装
フィールド型を`implements`できます、それがインタフェースを公開するクラスでなくとも!
そして、フィールドの同名メンバのコピーとして暗黙的に宣言・定義されます。
さらに、それを再宣言・定義(≒オーバーライド)することもできます。

```dart
extension type I64(int value) implements int {
  bool get isEven => value.isOdd; // lying
}

void main() {
  var i64 = I64(123);
  print(identical(123, i64)); // true
  print(i64.isOdd); // true
  print(i64.isEven); // true (deceived)
}
```

### フィールド型への代入

先の例では、`I64`は`int`のインターフェースを実装しているので`int`型の変数に代入できます。

ここでフィールド型で宣言・定義されたメンバを再宣言・定義していると、コンテクストによって動作が異なることになるため、混乱のもとです。

```dart
  int i = i64; // ok
  print(identical(i64, i)); // true
  print(i64.isEven); // true (deceived)
  print(i.isEven); // false (confusing)
```

従って、新たな[アノテーション`@redeclare`](https://pub.dev/documentation/meta/latest/meta/redeclare-constant.html "redeclare constant - meta library - Dart API")と[Lint`annotate_redeclares`](https://dart.dev/tools/linter-rules/annotate_redeclares "annotate_redeclares | Dart")が導入されました。

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

extension type I64(int value) implements int {
  @redeclare
  bool get isEven => value.isOdd; // lying
}
```

# 余談
### `extension type`というネーミング
メンバ(フィールド、メソッド、ゲッター、オペレータ等)の名前解決はコンパイル時に行えるものだけが許されます。
これは`extension`と同じであり、`extension type`の名前の由来です。

また、冒頭で述べた通りラッパーオブジェクトなどもともとありませんが、いわば常にアンボックスされているので、フィールドのインライン展開と言えます。
ラッパーオブジェクトのサイズ分省メモリであり、そのデリファレンスの時間分高速です。
機能名の候補にあがっていた`inline class`はここから来たのでしょう。
なお、`inline class`としてプレビューが動くところまで実装されましたが、クラスと呼ぶには特異な挙動が多すぎるという理由の[後付のクライテリア](https://github.com/dart-lang/language/issues/3207 "Introduce abstraction for specifying \"class-like\" structure · Issue #3207 · dart-lang/language")で却下され、`extension type`として再実装されました。

ちなみに、同等の機能を持つKotlinの`inline class`は`value class`と改名されましたが、Dartにおいて`value class`を名乗るのは不適当です。
なぜなら、実行時型が例外なくリファレンス型であるDartにおいて、"value"はリファレンスを持たないことを意味せず、それが不定であることを意味しますが、そもそも存在しないラッパーオブジェクトのリファレンスの議論はナンセンスだからです。

しかも、[Dartにおける`value class`](https://github.com/dart-lang/language/blob/main/working/value-classes/feature-specification.md "language/working/value-classes/feature-specification.md at main · dart-lang/language")はKotlinにおける`data class`相当になる予定です。(ややこしい)

他には、[`exetnsion class`、`typedef type`、`newtype`、Constricted type、`record class`、`static class`、`view class`、`view`、`facade class`、`skin class`等の候補名が有りました](https://github.com/dart-lang/language/issues/1974 "Can we come up with a better name than view? · Issue #1974 · dart-lang/language")。 (72 comments!)

### `int`の`extension type`はゼロボックスか?
Dartにおいて、`int`もご多分に漏れずリファレンス型であり、プリミティブ型ではありません。
また、`int`はKotlinの`Long`相当であって、Javaの`long`(常にアンボックス)や`Long`(常にボックス)相当ではありません。 
そして、その`int`のボックス、アンボックスは最適化イシューです。 

従って、Null安全で最適化によるアンボックスの機会が増えたとはいえ、`extension type`においても、`int`フィールドのアンボックスまで含めたゼロボックスを保証するわけではありません。

# おわりに
プリミティブ型を持たないほど「高級」であることを目指すDartにおいて、`extension type`は異色です。
`extension type`の目的がプリミティブ型とともに捨てたレベルの効率化、すなわちアンボックスにほぼ特化しているように見えるからです。
そして、アプリの実行効率はマイクロベンチマークでは決まりません。
そのわりに、デリケートで特異な挙動が少なからず存在します。

また、新機能採用基準に「実アプリケーションがあること」というのががあったはずです(参照先失念)。

次回はその[具体的なユースケース](https://qiita.com/Cat_sushi/items/87742dc3a886dd984f46 "Dart 3.2で導入したextension typeのユースケース")について書きたいと思います。