🦺

Null safety codelabやってみた

2021/07/26に公開

FlutterをやっていてDartのNull safetyがいまいち分からなかったので、公式が出しているcodelabをやってみました。こちらのcodelab、問題はあるものの解答らしきものはなかったので、自分の解答も合わせて書いておきます。

間違いなどありましたら、ご連絡いただければさいわいです!

今回やったもの

https://dart.dev/codelabs/null-safety

null-safe type systemについて

  • 導入: Dart 2.12

null safetyを導入すると明示的に指定しない限り、コード中で変数にnullを入れることができないようにできる。

コメント: ぬるぽとおさらばできるのはありがたいですね。

このcodelabで学べること

  • Nullable型とnon-nullable型について
  • nullを許容するかどうかを示す?!をいつ使うか
  • 構文解析とtype promotionのフロー
  • late演算子が変数と初期化に与える影響

コメント: type promotionってなに?

Nullable型とnon-nullable型について

null safety導入後はすべての型でnon-nullableがデフォルトになり、以下のanullを代入しようとするとコンパイルエラーになります。

void main() {
  int a;
  a = null; // Null can't be assigined
  print('a is $a.');
}

※コード内コメントは筆者が追記。

nullを許容したい場合は?を使って明示する必要があります。

  • 解答
void main() {
  int? a;
  a = null;
  print('a is $a.'); // a is null
}

Nullable type parameters for generics

ジェネリクスの型パラメータも同じくnullを許容するかどうかを指定できます(デフォルトはnon-nullable)

Exercise: Nullable type parameters for generics

  • 解答
void main() {
  List<String> aListOfStrings = ['one', 'two', 'three'];
  List<String>? aNullableListOfStrings;
  List<String?> aListOfNullableStrings = ['one', null, 'three'];

  print('aListOfStrings is $aListOfStrings.');
  print('aNullableListOfStrings is $aNullableListOfStrings.');
  print('aListOfNullableStrings is $aListOfNullableStrings.');
}

こちらの練習問題ですが、ポイントが2つあります。

  • Listそのものがnullであることを許容させたい
  • Listの要素にnullがあることを許容させたい

前者の対応するのが以下のコードです。

List<String>? aNullableListOfStrings;

後者はこちら。List要素の型がStringで、?がつくことによって要素がnullの場合を許容させています。

List<String?> aListOfNullableStrings = ['one', null, 'three'];

The null assertion operator (!)

  • nullable typeがnullでないことを表現したいときは!を使う

  • 解答

int? couldReturnNullButDoesnt() => -3;

void main() {
  int? couldBeNullButIsnt = 1;
  List<int?> listThatCouldHoldNulls = [2, null, 4];

  int a = couldBeNullButIsnt;
  int b = listThatCouldHoldNulls.first!; // first item in the list
  int c = couldReturnNullButDoesnt()!.abs(); // absolute value

  print('a is $a.');
  print('b is $b.');
  print('c is $c.');
}

couldReturnNullButDoesnt()関数とlistThatCouldHoldNullsint?とあるので、nullableな型になっています。
以下でそれぞれから値を取得しようとしたときにint型はnon-nullableな値のため、このままだとエラーになってしまいます。
なので、!を使ってnullじゃないことをプログラムに教えてあげればOKです。

Type promotion

  • nullになる可能性がある変数でも、nullにならないのであれば、non-nullableな変数として扱う

  • Definite assignment : Dartでは変数がどこで割り当てられ読み込まれたかを追跡でき、non-nullableな変数に値が代入されているかを検証できる。

以下のif-else文をコメントアウトすると、textに値が代入されないままprint(text)で使用されるためエラーになります。

void main() {
  String text;

// 以下のif文をコメントアウトすると、エラーになる
  if (DateTime.now().hour < 12) {
   text = "It's morning! Let's make aloo paratha!";
  } else {
   text = "It's afternoon! Let's make biryani!";
  }

  print(text);
  print(text.length);
}

lateキーワード

クラス変数やトップレベル変数はnon-nullableであるべきだが、値をすぐには代入できない場合がある。そういうときにはlateを使います。

  • late

    • あとで代入することを宣言できる。non-nullableな型で代入してなくてもエラーにならない
    • 代入されずに値が使用されるとLateInitializationErrorを投げる
    • latefinalと一緒に使うこともできる

  • 解答

class Meal {
  // lateをつけることで、あとから代入することを明示
  late String _description;

  set description(String desc) {
   _description = 'Meal description: $desc';
  }

  String get description => _description;
}

void main() {
  final myMeal = Meal();
  myMeal.description = 'Feijoada!';
  print(myMeal.description);
}
  • finalと使う
class Team {
  late final Coach coach;
}

class Coach {
  late final Team team;
}

void main() {
  final myTeam = Team();
  final myCoach = Coach();
  myTeam.coach = myCoach;
  myCoach.team = myTeam;

  print('All done!');
}

lazy initialization

lateをつけた変数は使用するときに初めて初期化されます
codelabのコードでみてみます。

_cachelateをつけないで実行する。

int _computeValue() {
  print('In _computeValue...');
  return 3;
}

class CachedValueProvider {
  final _cache = _computeValue();
  int get value => _cache;
}

void main() {
  print('Calling constructor...');
  var provider = CachedValueProvider();
  print('Getting value...');
  print('The value is ${provider.value}!');
}

🔽

Calling constructor...
In _computeValue...
Getting value...
The value is 3!

_cachelateをつけて実行する。

class CachedValueProvider {
  late final _cache = _computeValue();
  int get value => _cache;
  int _computeValue() {
      print('In _computeValue...');
      return 3;
   }
}

void main() {
  print('Calling constructor...');
  var provider = CachedValueProvider();
  print('Getting value...');
  print('The value is ${provider.value}!');
}

🔽

Calling constructor...
Getting value...
In _computeValue...
The value is 3!

前後の差分を取ってみます。

@@ -1,4 +1,4 @@
 Calling constructor...
-In _computeValue...
 Getting value...
+In _computeValue...
 The value is 3!

lateをつける前はコンストラクタでインスタンスを生成したあとに_computeValue()が呼び出されています。
lateをつけたあとは、getterで_cacheを取得するときに_computeValue()が呼び出されて値が初期化されていることがわかります。

参考

Discussion