ジェネリック型を単純に <T>
にばかりしないほうがいいと思ったのでメモします。
Foo<T>
ジェネリックなクラスを使うときに型を明示していなくてランタイムにも決まらない場合には dynamic
になります。
void main() {
final v = Foo().value; // dynamic
}
class Foo<T> {
T? value;
}
dynamic の扱いにくさ
analysis_options.yaml で strong-mode の implicit-dynamic: false
の指定をしていると、dynamic
な変数を使うときに明示的に何らかの型に cast しないとエラーになって扱いにくいです。
// Missing parameter type for 'v'.
<dynamic>['a', 1].map((v) => print('$v')).toList();
null safety が導入された Dart 2.12 以降では Object?
(non-null だと決まっているなら Object
)で代用すると避けることができます。
// vは自動的にObject型だと判断されてエラーにならない
['a', 1].map((v) => print('$v')).toList();
ただ implicit-dynamic: false
の設定をしていない場合には dynamic
でもそんなに困らないのかも知れません。
また、strong-mode は今はもう deprecated になっていて、下記のように strict-inference: true
などを使ったほうがいい そうですが、そちらはどうも緩くて警告しか出なかったり何の警告もなかったりするので、上の map の例でもエラーになりません。
analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
どちらにしても、型がまだ無いという dynamic
の状態は気持ち悪く感じます。
Foo<T extends Object?>
T extends Object?
に替えると、T
の具体的な型が示されていないときに dynamic
になるのを避けることができます。
void main() {
final v = Foo().value; // Object?
}
class Foo<T extends Object?> {
T? value;
}
Foo<T extends Object>
では、?
のない T extends Object
は何なのでしょうか。
void main() {
final foo = Foo(null); // 'Null' doesn't conform to the bound 'Object' of the type parameter 'T'.
}
class Foo<T extends Object> {
const Foo(this.value);
final T value;
}
null
になる可能性が排除されました。
まだまだ奥は深い
上記はわかっていれば難しくもない話です。
でもジェネリクスの nullability は結構ややこしいです。
class Foo<T extends Object?> {
const Foo([this.value]);
final T? value;
}
ここで T
は extends Object?
で null
も含まれるので T?
に近いものになっているイメージを持つのですが、コンストラクタのオプショナルな引数のためには final T value
ではなく final T? value
としなければなりません。
その一方で、仮引数や戻り値の型ではそうでもありません。
しかも次のようにますますややこしいです。
class Foo<T extends Object?> {
Foo(T value) : _value = value; // T?はダメ(a)
T _value;
T get value => _value; // TでもT?でもいい(b)
void updateValue1(T value) { // T?はダメ(c)
_value = value;
}
void updateValue2(T value) { // TでもT?でもいい(d)
if (value != null) {
_value = value;
}
}
b と d では T
と書くことも T?
と書くこともできます。
T
が null
を含んでいるので T?
と書く意味はなさそうですが、明確にするためにあえて T?
としてもいいかもしれません。
a と c についても、たった今書いた「T
が null
を含んでいるので」という理屈からすると T?
にできそうに思えますが、_value の型を T
にしているので value を T?
にすると _value = value
がエラーになります。
トリッキーですね。
このあたりの奥深さを以前につぶやいていたので最後に貼っておきます。
(スレッドになっていてツイートがもう一つありますが、その二つ目はちょっと怪しくて見直しが必要かもしれません。)
Dartのジェネリクスの型で時々混乱するのでメモ。 pic.twitter.com/lL7PDMGBGJ
— Kabo (@kabochapo) June 16, 2022