Dartの型の理解しておきたいあれこれ（Null safety編）

2021 年 3 月 3 日、ついに Dart 2.12 がリリースされて Dart は晴れて null-safe（null 安全）な言語となりました。
同日リリースされた Flutter 2 でも Dart 2.12 が同梱されています。

# 本記事について

この機会に、「[Dartの型の理解しておきたいあれこれ](https://qiita.com/kabochapo/items/925a2cee9199031272df)」に含めていた null safety 関連の情報を分離し、更に充実させることにしました。

ただし null safety 全般ではなく、あくまで型に焦点を置きます。
null safety 自体は今後当たり前に使われるものになっていくので、基本的すぎることは省いて見落としがちな部分を中心に扱います。

## 前提知識と資料

- Dart の型の基礎～応用
    - [Dartの型の理解しておきたいあれこれ](https://qiita.com/kabochapo/items/925a2cee9199031272df)
- null safety の全体像
    - [Understanding null safety | Dart](https://dart.dev/null-safety/understanding-null-safety)


# 型の比較

nullable な型（Null 許容型： `int?` のように `?` が付いた型）の変数は、`null` 以外の値が代入されると [flow analysis](https://dart.dev/null-safety/understanding-null-safety#flow-analysis) という解析によって non-nullable な型（Null 非許容型： `int` など）とみなされます。

```dart
int? v;
print(v.runtimeType);  // Null
print(v is int);       // false
print(v is int?);      // true

v = 10;                // この時点で non-nullable
print(v.runtimeType);  // int
print(v is int);       // true
print(v is int?);      // true
```

次のように `if` によって `null` の可能性を排除した場合も、その後ろでは non-nullable な `int` だと賢く判断してくれます。

```dart
final v = someFunc();  // int? を返す関数
print(v is int);       // false
print(v is int?);      // true

if (v == null) {
  return;
}

print(v.runtimeType);  // int
print(v is int);       // true
print(v is int?);      // true
```

`is int?` では、nullable（`int?`）と non-nullable（`int`）のどちらの値とも比較できます。

注目すべきは、`int` が確定した後はもう `int?` ではないのに `is int?` が `true` になる点です。
これは **non-nullable な型は nullable な型の派生型** だからです。[^subtype_of_nullable]
`int?` は `int` の親なので、通常の親と同様に `is` で比較できるのです。

[^subtype_of_nullable]: "The type system understands that a union type is a supertype of its branches. In other words, `int` is a subtype of `int?`." https://medium.com/dartlang/why-nullable-types-7dd93c28c87a#2ba9

もう一つ注目しておきたいのは、nullable な変数にまだ値が入っていない状態の型比較です。

```dart
String? v;
print(v is int?);  // true
```

`String?` として宣言した変数でも値が入っていなければ `is int?` が `true` になります。
不思議に思えるでしょうか？

変数に何も代入していなければ `null` であり、null は前述のとおり `Null` 型です。

```dart
String? v;
print(v is Null);  // true
```

`Null` 型には「`int?` の null」や「`String?` の null」という区別はありません。
そのため、null に関しては単に `v == null` という比較になります。
つまり `v is int?` では `v is int || v == null` という評価が為されます。

## Object との比較

「[Dartの型の理解しておきたいあれこれ](https://qiita.com/kabochapo/items/925a2cee9199031272df)」で確認したことの復習

- `Object` 型はあらゆる型の基底
- `is Object` は常に `true`

null safety においては Null 型はこれに当てはまりません。
そのため nullable な型も当てはまりません。

```dart:Dart2.10の場合
print(null is Object);  // true

int v;
print(v is Object);     // true
```

```dart:Dart2.12の場合
print(null is Object);  // false

int? v;
print(v is Object);     // false

v = 10;
print(v is Object);     // true
```

これは Dart が null safety に対応する過程において [変更された仕様](https://github.com/dart-lang/language/issues/141#issuecomment-448921322) です。
Object 型は `null` 以外を示すものになりました。

ちなみに、Object 型と Null 型には共通の基底クラスが存在していて、それが null-safe になる前の Object に近いもの（`Object?` に相当する無名のクラス）になっているようです。

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">There is essentially a hidden unnamed superclass of Object and Null that has toString(), hashCode, and operator ==(), which is what allows you to call those methods on nullable types.<br><br>Discussion: <a href="https://t.co/HrDKwWN8F6">https://t.co/HrDKwWN8F6</a>, <a href="https://t.co/OYJwRVj8r3">https://t.co/OYJwRVj8r3</a></p>&mdash; Bob Nystrom (@munificentbob) <a href="https://twitter.com/munificentbob/status/1253747300171563010?ref_src=twsrc%5Etfw">April 24, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

このような仕組みを知ると、nullable な型がどのように実現されているのかが見えてきますね。

## 型の nullability を判定

値の型を判定するのはここまでの情報で足りますが、ある型が nullable かどうかを知りたいときにはどれも使えなかったので、新たな記事として書きました。

https://blog.kaboc.cc/posts/20221222-01gmwrp7sg

# Never 型

`Never` は関数やメソッドの戻り値に使える特殊な型（[ボトム型](https://ja.wikipedia.org/wiki/%E3%83%9C%E3%83%88%E3%83%A0%E5%9E%8B)）です。

null safety の話ではないのですが、flow analysis が関わりがあって文脈に馴染むのでこの記事に含めました。
null safety が導入されるより前の Dart 2.9 で追加された型です。

ちょっとわかりにくいものなので、`Never` を使わない例で問題点を見てみましょう。

```dart
void main() {
  int? v;
  if (v == null) {
    throwException();
  }

  // An expression whose value can be 'null' must be
  // null-checked before it can be dereferenced.
  // というエラーになる
  print(v * 2);
}

// 必ず例外が発生する関数
void throwException() {
  throw Exception('Oops!');
}
```

これはコード内に注記したとおりエラーになります。
下記がその理由です。

- `v` が `null` なら `throwException()` が呼ばれて必ず例外が発生する
- 例外で止まればその後ろのエラー箇所には到達しない
    - 逆に言うと、`v` が `null` でなければ例外が発生せずに到達する
- 到達したときには `v` は non-nullable なので `v * 2` の算術ができるはず
    - しかし、コンパイラや linter はそれを判断できない ⇒ エラー

エラーをなくす対策方法としては、算術の前に null でないことをチェックする方法がありますね。

もう一つの対策として使えるのが `Never` です。
関数で処理が止まって何も返されないことを `Never` 型で明示できます。

```dart
Never throwException() {
  throw Exception('Oops!');
}
```

これで `if (v == null)` の該当時に処理が止まることをコンパイラ等が容易に理解できます。
そして、`print(v * 2);` の時点で `v` が non-nullable であることを判断できてエラーが出なくなります。

# super 引数を使いつつ型を狭める

[Dart 2.17 で super 引数が追加](https://github.com/dart-lang/sdk/blob/main/CHANGELOG.md#2170---2022-05-11) されました。

```dart
class Foo {
  const Foo(this.value);

  final int? value;
}

class Bar extends Foo {
  const Bar(super.value); // const Bar(int? value) : super(value); と同等
}
```

この場合、Bar の引数である value は Foo と同じく `int?` 型になります。
これを `int` 型に狭めて `null` を禁止したい場合、super の前に型を書いて実現できます。

```dart
class Bar extends Foo {
  const Bar(int super.value);
}
```

引数を `int` にしたならアクセスするときも `int` になっていると便利ですが、残念ながら自動的にはそうならず、次のような工夫が必要です。

```dart
class Bar extends Foo {
  const Bar(int super.value) : _value = value;

  final int _value;

  @override
  int get value => _value;
}
```

# nullable から non-nullable への変換

冒頭付近に書いた「flow analysis」により、変数の値が `null` でないことを確実に判断できる場合には自動的に non-nullable として扱われるようになります。

```dart:FlowAnalysis
int? value = getValue();
if (value != null) {
  // ここではvalueはint?ではなくintになる
}
```

また flow analysis が効かないケースで絶対に `null` でない場合には、手動で `!` を付けることで non-nullable な型にキャストできます。

```dart:!によるキャスト
final list = <int?>[1, 2, null];
list.remove(null);
print(list.runtimeType);  // List<int?>

final int value = list[0]!;
```

このどちらもできないケースがあります。

## List の中身

List の中身を丸ごと non-nullable にしたくなったとします。
どうすればできるのでしょうか。

たった今見た例でわかるように List から `null` を取り除くだけでは nullable なままです。
`null` との比較の条件で除去すれば flow analysis が効きそうに思えますが、それも効きません。

```dart
final list = <int?>[1, 2, null];
list.removeWhere((v) => v == null);
print(list.runtimeType);   // List<int?>

final list2 = list.where((v) => v != null).toList();
print(list2.runtimeType);  // List<int?>
```

要素だけを `!` でキャストすることもできません。

### 方法 1

`whereType<T>()` が使えます。
ジェネリック型として `int` を指定するだけで `int` だけの List になります。

```dart
final list = <int?>[1, 2, null];
final list2 = list.whereType<int>().toList();
print(list2.runtimeType);  // List<int>
```

### 方法 2

package:collection にある [whereNotNull()](https://pub.dev/documentation/collection/latest/collection/IterableNullableExtension/whereNotNull.html) という extension method が使えます。
上述の `whereType()` とほとんど変わりませんが、こちらはジェネリック型の指定が不要です。

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

...

final list = <int?>[1, 2, null];
final list2 = list.whereNotNull().toList();
```

こちらの方法はへぶんさんが Twitter に書かれていたのを参考にしました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">collection ライブラリに Iterable を非 null 化できる whereNotNull() あった。 <br>( Iterable&lt;T?&gt; 👉 Iterable&lt;T&gt; )<a href="https://t.co/s7MgSHVsp5">https://t.co/s7MgSHVsp5</a></p>&mdash; へぶん🦌Flutter/Firebase (@heavenOSK) <a href="https://twitter.com/heavenOSK/status/1381588167195721728?ref_src=twsrc%5Etfw">April 12, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

## Map の中身

List と同様に値が `null` の項目を消しただけでは、型としては non-nullable なままとなります。
また、Map には `whereType<T>()` や `whereNotNull()` は使えませんし、`map.entries` に対して使っても効きません。

### 方法

いろいろと試してみたのですが、下記のような地道な方法しか見つかりませんでした。

```dart
final map = <String, int?>{'a': 0, 'b': null, 'c': 2};
map.removeWhere((_, v) => v == null);
final map2 = map.cast<String, int>();
print(map2.runtimeType);  // CastMap<String, int?, String, int>

// 一行で書くなら
final map2 = (map..removeWhere((_, v) => v == null)).cast<String, int>();
```

`castFrom<String, int?, String, int>()` の結果は `Map<String, int>` 型になります。
`CastMap`（Map の派生型）と出力されていますが `runtimeType` がそうなっているだけであり、`Map<String, int>` と同様に扱えます。[^map]

[^map]: `Map` は抽象クラスであり、それを実装した様々なクラスがあります。例えば `{'a': 1}` は実は `_InternalLinkedHashMap<String, int>` という型です。

これより良い方法を見つけた方はぜひお知らせください！


# おまけ

flow analysis が効かないケースで悩むことが多そうですのでまとめておきます。
型に関連する話ではありますが、直接的な話ではないのでおまけとしました。
おまけにしては長いですが、効かなくて困ったときにでもお読みください。

## Flow analysis が効かないケース

クラスのプロパティには flow analysis が効きません。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Dartのパッケージのnullsafety対応を試し始めたけど、インスタンスプロパティ（下記ではcallback）にnullを許可してる場合は<br><br>if (callback != null) {<br>　callback();<br>)<br><br>のようにifでnullチェックしただけじゃ警告が消えなくて、そのifブロックの中で使用時にcallback!()としないといけないみたい…。</p>&mdash; Kabo (@kabochapo) <a href="https://twitter.com/kabochapo/status/1333032234106851329?ref_src=twsrc%5Etfw">November 29, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

そのプロパティと同じクラス内で使おうとするときだけでなく、クラスのオブジェクト経由で他の箇所で使うときも同様です。

```dart
class Foo {
  int? value;

  ...
}

void main() {
  final foo = Foo();
  if (foo.value != null) {
    final int v = foo.value;  // エラー
  }
}
```

このコードでは、`foo.value` を `int` 型の変数である `v` に入れようとするところで「A value of type 'int?' can't be assigned to a variable of type 'int'.」というエラーになります。

### 理由

しばらくしてから Twitter の情報で理由がわかりました。
バグのように思えましたが仕様でした。

- [Dart null safety doesn&#39;t work with class fields - Stack Overflow](https://stackoverflow.com/a/65457221/12086560)

この Stack Overflow の回答にあるコードを見ながら解説していきます。

```dart
class A {
  final String? text = 'hello';

  String? getText() {
    if (text != null) {
      return text;
    } else {
      return 'WAS NULL!';
    }
  }
}

class B extends A {
  bool first = true;

  @override
  String? get text {
    if (first) {
      first = false;
      return 'world';
    } else {
      return null;
    }
  }
}
```

A クラスを継承した B クラスで `text` をオーバーライドしてゲッターに変えています。
親クラスにおいて `final` であっても子クラスでオーバーライドできます。

```dart:ゲッターの中身
if (first) {
  first = false;
  return 'world';
} else {
  return null;
}
```

ゲッターが一度目に呼ばれたときには文字列、二度目以降は `null` が返るようになっています。

さて、この `text` というゲッターが呼ばれるタイミングはわかりますか？

・・・

答えは、`getText()` 内で二度です。気づけましたか？
一度目は `if (text != null)`、二度目は `return text;` です。

- 二度目は `null` を返す
- そのときには既に if ブロックの中なので `if (text != null)` は再評価されない

**null チェックの後に null に変わることがある** わけです。
そのため、`getText()` の戻り値の型を `String?` から `String` に変えるとエラーになります。
そこまで考慮して nullable の可能性を警告してくれているなんて頼もしいですね！

なお、ゲッターでなくても起こるかどうかは不明です。
もし非同期にプロパティを書き換える処理があれば起こり得るかもしれません。
その場合はオーバーライドは無関係に起こる気がします。

### 対策

**方法1**

理由がわかる前は私は `!` を使っていました。
`!` は nullable でないと断定して non-nullable な型にキャストするものです。

```dart
if (text != null) {
  return text!;
}
```

静的解析では `return text;` のところで `null` かどうか判断できないため警告してくれませんが、実行時に `null` になっていればキャストできずにエラーが発生します。
動作を把握できていないクラスの場合は、そのエラーの発生によって実行が止まってしまう可能性があるため、少しでも不安があれば確実に避けておくのが良いと個人的には考えます。
一方、問題を起こすゲッターがないことを確認してわかっていればこの方法で足りると思います。

**方法2**

if の前にローカル変数に入れて使う方法です。
その変数の `null` チェックを行い、それ以後はクラスのプロパティの代わりに使いましょう。
if ブロック内で危険なプロパティを避ければチェック後に `null` に変わることはなくなります。
ただし、ゲッターによって値が変わってもローカル変数は変わらないままになり、それがかえって良くない場合もあるかもしれません。

※@Cat_sushi さん、コメントありがとうございました！

### 様々なケース

null チェックをしても non-nullable な型にならないケースは他にもあり、その多くを解説するページが Dart のドキュメントに追加されました。

https://dart.dev/tools/non-promotion-reasons

コードを書いていておかしいと思ったときにはこのページを見ればいいですね。
起こる理由はほとんどが似ているので、先ほどの例を理解していればわかりやすいと思います。

---

null safety の理解が深まったでしょうか？
本記事では落とし穴のような部分を多く取り上げたので怖くなってしまったかもしれませんが、Dart は単なる null safety ではなく「sound null safety」[^soundness] であり、メリットのほうが大きいと思います。

[^soundness]: 詳しくは [The Dart type system](https://dart.dev/guides/language/type-system) の中の [What is soundness?](https://dart.dev/guides/language/type-system#what-is-soundness) をお読みください。

今後は null safety と付き合っていくことになりますので、悩んだら読み直して慣れていきましょう。


# 参考

- [Understanding null safety | Dart](https://dart.dev/null-safety/understanding-null-safety)
- [The Dart type system | Dart](https://dart.dev/guides/language/type-system)
- [Why nullable types?. Null in Dart code indicates that a… | by Bob Nystrom | Dart | Dec, 2020 | Medium](https://medium.com/dartlang/why-nullable-types-7dd93c28c87a)
- [Fixing type promotion failures | Dart](https://dart.dev/tools/non-promotion-reasons)