Closed50

DartのNull safetyについて調べる

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

変数はデフォルトで非Nullとなる、Nullの代入はコンパイラエラーとして警告される

void main() {
  var i = 42;
  i = null; // A value of type 'Null' can't be assigned to a variable of type 'int'.
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Introduction through examples

nullを代入可能にするには型の後にクエスチョンマークを付ける

void main() {
  int? nullable;
  nullable = null;
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Enabling/disabling null safety

Sound null safetyはDart 2.12以上またはFlutter 2.0から利用可能

有効にするには例えばpubspec.yamlに下記のように記述する

pubspec.yaml
environment:
  sdk: '>=2.12.0 <3.0.0'
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Migrating an existing package or app

下記のコマンドを実行してnull safetyではないソースコードのマイグレーションが可能

dart migrate
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Understanding null safety

// Without null safety:
bool isEmpty(String string) => string.length == 0;

main() {
  isEmpty(null);
}

Null safetyではないと上記がコンパイルを通ってしまって実行時にクラッシュする

サーバーアプリケーションならカバーできないこともないがモバイルアプリはカバーしようがない

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Dartのnull safetyの原則

  • コードはデフォルトで安全であるべきである
  • null safeなコードが書きやすくあるべきである
  • 出来上がるコードは完全に堅牢であるべきである
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

出来上がるコードは完全に堅牢であるべきである

Dartのnull safetyにおけるsoundnessが説明されている

For us, in the context of null safety, that means that if an expression has a static type that does not permit null, then no possible execution of that expression can ever evaluate to null.

もし式がnullを許容しない静的な型を持っているならば、式の取り得る評価結果は決してnullにならない

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

堅牢性によってnullでないことを仮定できればコンパイラは最適化が可能

結果としてより小さいサイズかつ高速なバイナリを生成できる

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Nullability in the type system

訳するとタイプシステムにおけるnullability(nullになる可能性)

nullは全ての型との間にis-a関係があるように扱われる

null値はいかなるメソッドやオペレーターを持たない

持たないのに呼び出そうとするからクラッシュする

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Non-nullable and nullable types

訳すると非Null許容とNull許容の型

Null safetyは型の階層構造を変えることでnullによるクラッシュの恐れを低減する

Null safetyの型の階層構造ではnullと全ての型との間にis-a関係があるように扱わない

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida
void main() {
  makeCoffee("coffee");
  makeCoffee("coffee", "dairy");
}

makeCoffee(String coffee, [String? dairy]) {
  if (dairy != null) {
    print('$coffee with $dairy');
  } else {
    print('Black $coffee');
  }
}

知らなかったけど省略可能引数とかvoid省略とかできるんだね

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Using nullable types

訳するとNull許容の型を使う

bad(String? maybeString) {
  print(maybeString.length);
}

main() {
  bad(null);
}

下記のコンパイルエラーが発生して実行できない

The property 'length' can't be unconditionally accessed because the receiver can be 'null'.

null値でも下記は使えるらしい

  • toString
  • ==
  • hashCode
bad(String? maybeString) {
  print(maybeString.toString()); // null
  print(maybeString.hashCode); // 0
  print(maybeString == null); // true
}

main() {
  bad(null);
}

たしかに使える、従ってMapのキーなどにも使える

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida
requireStringNotNull(String definitelyString) {
  print(definitelyString.length);
}

main() {
  String? maybeString = null; // Or not!
  requireStringNotNull(maybeString);
}

上記の場合はコンパイルエラーが発生する

The argument type 'String?' can't be assigned to the parameter type 'String'.

StringとString?の間にはis-a関係がある

子クラスであるStringの引数に親クラスであるString?(null値)を渡そうとしているからエラーになる

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida
requireStringNotObject(String definitelyString) {
  print(definitelyString.length);
}

main() {
  Object maybeString = 'it is';
  requireStringNotObject(maybeString);
}

ちょっと内容は異なるけれど同じ理由でエラーになる

The argument type 'Object' can't be assigned to the parameter type 'String'.

暗黙的にダウンキャスト(親クラスや先祖クラスから子クラスへのキャスト)することはできない

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida
requireStringNotObject(String definitelyString) {
  print(definitelyString.length); // 5
}

main() {
  Object maybeString = 'it is';
  requireStringNotObject(maybeString as String);
}

明示的にキャストすればエラーもでないしクラッシュもしない

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida
List<int> filterEvens(List<int> ints) {
  return ints.where((n) => n.isEven);
}

main() {
  print(filterEvens([1, 2, 3]));
}

上記はうまくいきそうだけどコンパイルエラーになる

A value of type 'Iterable<int>' can't be returned from the function 'filterEvens' because it has a return type of 'List<int>'.

理由はwhereメソッドの戻り値がイテラブルなので暗黙的にリストには変換できないため

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Top and bottom

直訳すると最上位と最下位

最後の箇条書き以外は読み飛ばしても良いとのことだったので読み飛ばして必要なら読もう

  • 最上位のクラスはObjectではなくObject?である
  • 最下位のクラスはNullではなくNeverである

Neverってなんだろうって調べたらわかりやすい記事を見つけた

https://typescriptbook.jp/reference/statements/never

必ず例外が発生する関数や無限ループの関数の戻り値やswitch文の網羅性チェックに便利そう

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Ensuring correctness

Null safetyによる静的解析が機能するのは主に下記の3つ

  • 代入
  • 引数
  • 戻り値

Invalid returns

String missingReturn() {
  // No return.
}

上記のコードでは下記のコンパイルエラーが発生する

The body might complete normally, causing 'null' to be returned, but the return type, 'String', is a potentially non-nullable type.

何もreturnしなければ戻り値はnullになるがStringはnullを許容しないのでエラーになる

Uninitialized variables

Dartでは変数を定義する時に何も指定しないとデフォルト値がnullになる

グローバル変数とクラスのスタティックフィールドは宣言時の初期化が必須

int topLevel;

class SomeClass {
  static int staticField;
}
The non-nullable variable 'topLevel' must be initialized.
The non-nullable variable 'staticField' must be initialized.

インスタンスフィールドは宣言時に初期化するかコンストラクターで初期化する

class SomeClass {
  int atDeclaration = 0;
  int initializingFormal;
  int initializationList;

  SomeClass(this.initializingFormal)
      : initializationList = 0;
}

初期化しない場合

class SomeClass {
  int uninitialized;
  SomeClass();
}
Non-nullable instance field 'uninitialized' must be initialized.

コンストラクターのボディーに来るまでに初期化されていればOK

関数やメソッド内のローカル変数は初期化が不要だが、使用する前に値を代入する必要がある

int tracingFibonacci(int n) {
  int result;
  if (n < 2) {
    result = n;
  } else {
    result = tracingFibonacci(n - 2) + tracingFibonacci(n - 1);
  }

  print(result);
  return result;
}

ない場合

int tracingFibonacci(int n) {
  int result;
  if (n < 2) {
    result = n;
  }

  print(result);
  return result;
}
The non-nullable local variable 'result' must be assigned before it can be used.
The non-nullable local variable 'result' must be assigned before it can be used.

オプショナルな引数にはデフォルト値が必要

引数がNullableな場合はnullとなるが、それ以外の場合はエラーとなる

void myPrint([String message]) {
  print(message);
}
The parameter 'message' can't have a value of 'null' because of its type, but the implicit default value is 'null'.

これらの制約は面倒だが堅牢なコーディングを行う上では便利

例えばfinalは再代入できない制約があるが保証もあるので意図しない再代入を防ぐことができる

そうは言っても制約は煩わしい場合がある

これらの制約によるコーディングしにくさを緩和するDartの言語機能があるらしい、楽しみ

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Flow analysis

フロー解析

bool isEmptyList(Object object) {
  if (object is List) {
    return object.isEmpty;
  } else {
    return false;
  }
}

上記ではif (object is List)によってobjectがListであることがコンパイラは解析できる

従ってListクラスのプロパティであるisEmptyを呼び出すことができる

こういうのをタイププロモーションと呼ぶらしい

Null safety以前は下記でタイププロモーションが行われなかったらしい

bool isEmptyList(Object object) {
  if (object is! List) return false;
  return object.isEmpty;
}

void main() {
  print(isEmptyList([]));
}

Null safetyでは問題なくコンパイルも通るし、実行時エラーも発生しない

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Reachability analysis

到達性分析

returnの他にもbreakやthrowがブロックを終了させる言語としての機能を持つ

例えば下記は普通にコンパイルが通る

void func(Object o) {
  for (var i = 0; i < 10; i += 1) {
    if (o is! List) {
      break;
    }
    print(o.isEmpty);
  }
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Never for unreachable code

到達しないコードのためのNever型

Neverは到達しないことを保証するのに使える

到達する可能性がある場合はコンパイラエラーとして捕捉できる

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

下記は必ず例外が発生するので戻り値はないのでNeverでOK、もし戻り値がある場合

Never wrongType(String type, Object value) {}
The body might complete normally, causing 'null' to be returned, but the return type, 'Never', is a potentially non-nullable type.

例外を投げるヘルパー関数にはvoidよりもNeverの方が向いている

下記のような例ではwrongType関数が呼び出された時点でそれ以後はotherPointであることが確定する

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...
}

したがってタイププロモーションが行われてotherがPointだとわかるのでxyなどのプロパティを呼び出せる

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Definite assignment analysis

確定代入分析

int tracingFibonacci(int n) {
  final int result;
  if (n < 2) {
    result = n;
  } else {
    result = tracingFibonacci(n - 2) + tracingFibonacci(n - 1);
  }

  print(result);
  return result;
}

ローカル変数が使用される前に代入が済んでいるかどうかを解析する

finalって初期化しなくても良いんだ、知らなかった

初期化されていないケースがある場合はエラーとなる

int tracingFibonacci(int n) {
  final int result;
  if (n < 2) {
    result = n;
  }

  print(result);
  return result;
}
The final variable 'result' can't be read because it's potentially unassigned at this point.
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Type promotion on null checks

Nullチェックによるタイププロモーション

String makeCommand(String executable, [List<String>? arguments]) {
  var result = executable;
  if (arguments != null) {
    result += ' ' + arguments.join(' ');
  }
  return result;
}

if (arguments != null)によってブロック内がnullではないことが保証される

従ってjoinメソッドを呼び出すことができる

!= nullだけではなく== nullを使うことも可能

String makeCommand(String executable, [List<String>? arguments]) {
  var result = executable;
  if (arguments == null) return result;
  return result + ' ' + arguments.join(' ');
}

セクションの最後にタイププロモーションはローカル変数にだけ働きますと書いてある、なぜだろう

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Unnecessary code warnings

不要なコードに関する警告

String checkList(List<Object> list) {
  if (list?.isEmpty ?? false) {
    return 'Got nothing';
  }
  return 'Got something';
}

list??は不要なので警告が表示される

The receiver can't be null, so the null-aware operator '?.' is unnecessary.
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Working with nullable types

Null許容タイプを扱う

Smarter null-aware methods

スマートなNull認知メソッド

void main() {
  String? notAString = null;
  print(notAString?.length);
}

今更だけど?.オペレーターってオブジェクトの方がnullだったらnullに評価されるんだね

void main() {
  String? notAString = null;
  print(notAString?.length.isEven);
}

isEvenの前に?はいらない?

(notAString?.length)はInt?型なので必要そうな感じもする

答えは?はなくても大丈夫

理由はDartでは?.が論理演算子のように短絡するようになっているから

前の例ではnotAStringがnullだった時点でisEvenは評価されなくなる

仮にlengthInt?型だったら.ではなくて?.でアクセスする必要がある

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

こんなのもある

receiver?..method();
receiver?[index];

前者はreceiverがnullだったらnullになって、それ以外の場合はmethodを呼び出してreceiverを返す

後者はreceiverがnullだったらnullになって、それ以外の場合はindex番目の要素にアクセスする

前者だが2行目以降は..でいいのかな?

void main() {
  final list = null as List<int>?;
  
  list?..add(1)
    ..add(2);
  
  print(list); // null
}

良いようだ

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Null assertion operator

Null言明演算子

class HttpResponse {
  final int code;
  final String? error;

  HttpResponse.ok()
      : code = 200,
        error = null;
  HttpResponse.notFound()
      : code = 404,
        error = 'Not found';

  
  String toString() {
    if (code == 200) return 'OK';
    return 'ERROR $code ${error.toUpperCase()}';
  }
}

上記の例ではerrorがString?型なのでコンパイルエラーが発生する

The method 'toUpperCase' can't be unconditionally invoked because the receiver can be 'null'.

でもcodeが200でなければerrornullではない

少なくともそのように使うようにこのクラスは意図されている

asキーワードを使うことでキャストすることでコンパイルエラーを消せる

return 'ERROR $code ${(error as String).toUpperCase()}';

コーディングミスでerrorがnullだと実行時エラーが発生する

キャストの代わりに!演算子を使うこともできる、こちらの方が簡単

return 'ERROR $code ${error!.toUpperCase()}';
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Late variables

下記の例ではserveメソッドが呼び出される前に_tempratureが初期化されていない恐れがある

class Coffee {
  String? _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }

  String serve() => _temperature! + ' coffee';
}

void main() {
  final coffee = Coffee();
  coffee.serve(); // Uncaught TypeError: Cannot read properties of null (reading '$add')Error: TypeError: Cannot read properties of null (reading '$add')
}

エラーそのものを静的解析で補足することは残念ながらできない

ただしlateキーワードを使うことで?!などのコーディングを減らすことができる

class Coffee {
  late String _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }

  String serve() => _temperature + ' coffee';
}

void main() {
  final coffee = Coffee();
  coffee.serve(); // Uncaught Error: LateInitializationError: Field '_temperature' has not been initialized.
}

また_tempratureに誤ってnullを代入するといったエラーも防ぐことができる、late便利ですね

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Lazy initialization

lateキーワードには別の使い方もある

class Weather {
  late int _temperature = _readThermometer();
}

上記の例では_tempratureに初めてアクセスされる時に_readThermometerメソッドが呼び出される

初期化の処理が重い場合や必ずしも必要がない場合はとりわけ便利

class Weather {
  late int temperature = _readThermometer();
  
  int _readThermometer() {
    print("_readThermometer called");
    return 30;
  }
}

void main() {
  final weather = Weather();
  print("start");
  print(weather.temperature);
  print(weather.temperature);
}
start
_readThermometer called
30
30

たしかにコンストラクターの時には呼び出されずに最初に使用する時に呼び出されていることがわかる

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Late final variables

latefinalを組み合わせることもできる

class Coffee {
  late final String _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }

  String serve() => _temperature + ' coffee';
}

その場合は初期化を2回しようとすると実行時エラーが発生する

class Coffee {
  late final String _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }

  String serve() => _temperature + ' coffee';
}

void main() {
  final coffee = Coffee();
  
  coffee.heat();
  print(coffee.serve()); // hot coffee
  
  coffee.chill(); // Uncaught Error: LateInitializationError: Field '_temperature' has already been initialized.
  print(coffee.serve());
}

KotlinやSwiftにもlateinitlazyという同じようなキーワードがあるらしい

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Required named parameters

requiredキーワードを使うことでオプショナル引数を必須にできる

void main() {
  func(n: 1);
}

void func({required int n}) {
  print("n = $n"); // n = 1
}

requiredなオプショナル引数はint?などのnullableでもOK、その場合はnullを渡せる

requiredではないオプショナル引数はデフォルト値があればintなどのnon-nullableでもOK

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Abstract fields

contentsというフィールドを持つCupというフィールドを定義したい場合:

abstract class Cup {
  String contents;
}

上記のようにすると下記のエラーが発生する

Non-nullable instance field 'contents' must be initialized.

理由はcontentsが宣言ではなく定義として扱われるので初期化が必要だから

contentsというフィールドを持つインタフェースであること指定したい場合:

abstract class Cup {
  abstract String contents;
}

class MyCup implements Cup {
  
  String contents = "Coffee";
}

下記のようにしても良い

abstract class Cup {
  String get contents;
  set contents(String str);
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Working with nullable fields

下記は動きそうだが実際にはコンパイルエラーが表示される

class Coffee {
  String? _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }

  void checkTemp() {
    if (_temperature != null) {
      print('Ready to serve ' + _temperature + '!');
    }
  }
}

理由はクラスのフィールドではnon-null型へのプロモーションが行われないから

例えば_temperatureのゲッターに_tempratureを更新するコードが含まれている可能性を否定できない、可能性はかなり低いが

このような場合は下記のようにローカル変数に代入すればOK

class Coffee {
  String? _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }

  void checkTemp() {
    final temperature = _temperature;
    if (temperature != null) {
      print('Ready to serve ' + temperature + '!');
    }
  }
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Nullability and generics

ジェネリクスでnullableを使うのは面倒

class Box<T> {
  final T object;
  Box(this.object);
}

main() {
  Box<String>('a string');
  Box<int?>(null);
}

Tはnullableかもしれないしnon-nullableかもしれないのでそれぞれの制約を受ける

  • nullable: nullの可能性があるのでtoStringやhashCodeメソッドくらいしか呼び出せない
  • non-nullable: 必ず初期化する必要がある

T?のようにして明示的にnullableにすることで制約を受けないようにできる

class Box<T> {
  T? object;
  Box.empty();
  Box.full(this.object);

  T unbox() => object as T;
}

main() {
  var box = Box<int?>.full(null);
  print(box.unbox());
}

object as Tの代わりにobject!を使うとobjectがnullの時に例外が発生するので注意が必要

object as Tで大丈夫な理由はTがint?なのでnullであったとしてもキャストできるから

制約を回避する方法としてextendsを使用する方法もある

class Interval<T extends num> {
  T min, max;

  Interval(this.min, this.max);

  bool get isEmpty => max <= min;
}

main() {
  final interval = Interval<double>(0, 1);
  print(interval.isEmpty); // false
}

Interval<double>Interval<double?>の場合は下記のエラーが発生する

'double?' doesn't conform to the bound 'num' of the type parameter 'T'.

下記のようにextendsの対象としてnullableなタイプも指定できる

class Interval<T extends num?> {
  T min, max;

  Interval(this.min, this.max);

  bool get isEmpty {
    var localMin = min;
    var localMax = max;

    // No min or max means an open-ended interval.
    if (localMin == null || localMax == null) return false;
    return localMax <= localMin;
  }
}

この場合はTはnullableでもnon-nullableでもどちらでも良い

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

The Map index operator is nullable

Mapの添字演算子の戻り値がNullable

void main() {
  var map = {'key': 'value'};
  print(map['key'].length);
}

上記はコンパイルエラーが発生する

The property 'length' can't be unconditionally accessed because the receiver can be 'null'.

このような場合は非ヌル言明演算子!を使う

void main() {
  var map = {'key': 'value'};
  print(map['key']!.length); // 5
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Cannot access Iterator.current before or after iteration

イテレーションの前後にIterator.currentにアクセスできない

void main() {
  final list = [1, 2, 3];
  final cursor = list.iterator;
  
  print(cursor.current); // Error

  while (cursor.moveNext()) {
    print(cursor.current);
  }
  
  print(cursor.current); // Error
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Summary

  • 型はデフォルトでnon-nullable、nullableにするには?を使用する
  • オプショナル引数はnullableかデフォルト値がある必要がある、requiredを使うことで必須にできてその場合は非nullableにできる(デフォルト値は指定できない)、non-nullableのグローバル変数と静的変数には初期化が必ず必要、non-nullableなクラスのフィールドはコンストラクタのボディの前に初期化が必要
  • ?.&&||のように短絡する、?..?[]などのバリエーションがある、後置!でnon-nullableにキャストできる
  • フロー解析によってローカル変数はnon-nullへ型プロモーションができる
  • lateキーワードによってnon-nullかつfinalなフィールドを後から初期化することや初回使用時に飲み初期化することができる
  • Listクラスはnull safetyのために変更された
このスクラップは2023/01/10にクローズされました