見出し画像

DartのNullSafetyについて確認してみる1

Flutter2からNullSafetyに対応したDartが使えるようになった。そこで、NullSafetyに対応したことによって、何が出来るようになったのかを確認しておこうと思う。

NullSafetyとは

nullになる可能性があるかどうかを表現できる仕組み。

NullSafetyに対応していない時は `int hoge` と宣言した場合 hoge には null が入ってくる可能性がある。しかし、NullSafetyに対応している時は `int hoge` と宣言した場合は null が入ってこない、という扱いにできる。逆に、 `int? hoge` と宣言すると null が入ってくる場合がある、という扱いにできる。

int hoge = 1; // nullは代入できない
int? hoge = null; // nullを代入できる

で、NullSafetyに対応したことで何が嬉しいのかというと、意図せずnullが入ってきてNoSuchMethodError等になってしまう状況を防ぎやすくなること。

つまり、nullになること自体が悪なのではなく、nullになる可能性があるか分からない状態が悪であり、それの1つの解決策として nullになる可能性があるかどうかを表現できるようにした。

Nullability in the type system

まず、List や int など各タイプにはメソッドやプロパティが定義されているが、nullにはそういった物が定義されていない。なので、null に対してメソッドを呼んだりしてしまうとエラーになってしまう。

で、なぜそうなっていたかというと、Nullが各タイプのサブタイプになっていたのが理由らしい。そこで、NullSafety対応後はNullだけ特別扱いし、独立したタイプとして切り離した。

また、切り離されたNullには toString(), ==, hasCode が定義されているので、それらは使える。`null.toString()` はOKである。

画像1

NullSafetyに対応した後はキャストのされ方も少し異なるらしい。対応前は暗黙的にダウンキャストされていたが、そういった仕組みは排除された。

Object a = 'hoge';
String b = a; // NullSafety対応前はOK、対応後はエラーとなる
String c = a as String; // 明示的にキャストすれば対応後もOK

Ensuring correctness

関数内で何もreturnしなかった場合はnullを返していた。なので、NullSafety対応後は↓の様な関数はエラーとなる。

String helloWorld() {
  // 返り値は String を期待しているので、何もreturnしないのはエラーとなる
}

Non−Nullableな変数の初期化処理は、定義する場所よって少し扱いが変わるらしい。
・グローバル変数・static変数は初期化が必要
・ローカル変数は初期化が不要

int a = 1; // 初期化が必要

class Fuga {
  static int b = 2; // 初期化が必要
}

void hoge(int n) {
  String s; // 初期化しなくても良い
  if (n == 1) {
    s = 'HELLO WORLD';
    print(s); // これは s に値が入るのでOK
  } else {
    print(s); // s に値が入らないのでエラーとなる
  }
}

Flow analysis

記述されたコードの解釈も変わっているらしい。

まず、if文でタイプの判定を行った時、素直に書いた場合は判定されたタイプとして扱うことができる。これは、NullSafety対応前から可能。

bool isEmptyList(Object object) {
 if (object is List) {
   return object.isEmpty; // <-- OK!
 } else {
   return false;
 }
}

しかし、if (object is! List) return false; の様に、タイプ判定を行いつつ早期リターンした場合はタイプをいい感じに解釈してくれなかった。NullSafety対応後はそれが改善され、いい感じに解釈してくれるらしい。

bool isEmptyList(Object object) {
 if (object is! List) return false;
 return object.isEmpty; // NullSafety対応後は動く
}

Nullに関しても同様で、== null や != null をいい感じに解釈してくれるようになっている。

String hoge(String? message) {
 if (message == null) {
   return 'NULL';
 }
 return message.trim(); // OK
}

String fuga(String? message) {
 if (message != null) {
   return message.trim(); // OK
 }
 return 'NULL';
}

次に、到達不能コードにはNeverを使うらしい。NeverはNullSafety対応後に登場したタイプで、全てのタイプのサブタイプだと。

画像2

具体的な使い方はこんな感じ。throw自体がNeverを表現しているのでこうなるらしい。

Never wrongType(String type, Object value) {
 throw ArgumentError('Expected $type, but was ${value.runtimeType}.');
}

class Point {
 final double x, y;

 bool operator ==(Object other) {
   if (other is! Point) wrongType('Point', other);
   return x == other.x && y == other.y;
 }

 // Constructor and hashCode...
}

次に、変数の初期化に関してもいい感じに解釈してくれるらしい。NullSafety対応前は final String message; のように、宣言と同時に初期化してないとエラーとなっていたが、NullSafety対応後は後から必ず初期化されるのであればOKとなっている。

String hoge(int n) {
 final String message;
 if (n == 1) {
   message = 'one'; 
 } else {
   message = 'etc';
 }
 return message;
}

最後に

Nullになる可能性があるかどうかを表現できる程度かと思っていたが、新しいタイプが登場したり、コードの解釈も改善されていた。地味ではあるがこういう所を理解しておくとより安全なコードを、より理解しやすい形で書けるようになるかもしれない。

後半は次回の投稿で読み込んで行きたいと思う。


この記事が気に入ったらサポートをしてみませんか?