KabologHomeXGitHub

<T> と <T extends Object?> と <T extends Object>

ジェネリック型を単純に <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;  
}  

ここで Textends 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? と書くこともできます。
Tnull を含んでいるので T? と書く意味はなさそうですが、明確にするためにあえて T? としてもいいかもしれません。

a と c についても、たった今書いた「Tnull を含んでいるので」という理屈からすると T? にできそうに思えますが、_value の型を T にしているので value を T? にすると _value = value がエラーになります。
トリッキーですね。

このあたりの奥深さを以前につぶやいていたので最後に貼っておきます。
(スレッドになっていてツイートがもう一つありますが、その二つ目はちょっと怪しくて見直しが必要かもしれません。)

Xに投稿する