🍺

Dartのflow analysisについて理解する

2023/09/17に公開

TL;DR

  • params.valueでnullable判定できなくなったらローカル変数に定義し直したらエラー回避できるよ

Dartにてnullableなプロパティを扱うことはままあるでしょう。

そのなかで以下のコードではnullチェックしているのに、nullableな判定が外れず、null assertionが必要になることがあります。

if (params.value != null)
Hoge(value: params.value) // error! need `value!`

しかし、これはローカル変数に再定義することで型エラーは解消されます。

final value = params.value;
if (value != null)
Hoge(value: value) // no error

この挙動について整理してみようと思います。

flow analysis

定義されていた型から、ステートメントによって型が promoted (昇格) される動作です。これはDartのコードではよく行われているでしょう。

// With (or without) null safety:
bool isEmptyList(Object object) {
  if (object is List) {
    return object.isEmpty; // <-- OK!
  } else {
    return false;
  }
}

このコードではObject型で定義されていたものを is で判定することで、そのブロック内ではobjectList型であると認識されています。

(と、書いたもののほとんど公式ドキュメントのママの説明になっています)
https://dart.dev/null-safety/understanding-null-safety

では、この仕様がどう繋がるかというと、flow analysisは「引数のオブジェクト」や「ローカル変数」には適用されるが、「オブジェクトのフィールド・プロパティ」には適用されない(その能力がない)ということです。

実際に、以下のコードではエラーが発生します。

class Params {
  final String? name;

  const Params({this.name});
}

class Argument {
  final Params? params;

  const Argument({required this.params});
}

checkParams(Argument args) {
  if (args.params is Params) {
    print('params: ${args.params.name}');
  }
}

void main() {
  final optionalParams = Params();
  final args = Argument(params: optionalParams);
  checkParams(args);
}

dart.pad で実行すると以下のエラーになります。

// console
Error compiling to JavaScript:
lib/main.dart:15:34:
Error: Property 'name' cannot be accessed on 'Params?' because it is potentially null.
 - 'Params' is from 'package:dartpad_sample/main.dart' ('lib/main.dart').
    print('params: ${args.params.name}');
                                 ^^^^
lib/main.dart:8:17:
Info: 'params' refers to a property so it couldn't be promoted.
  final Params? params;
                ^
Error: Compilation failed.

では checkParams(Argument args)checkParams(Params params)に変えてみましょう。

 checkParams(Params params) {
  if (params is Params) {
    print('params: ${params.name}');
  }
}

void main() {
  final optionalParams = Params();
  checkParams(optionalParams);
}

エラーは発生せず、flow analysisによって、non-nullableにpromotedされ、実行されました。

// console
params: null

これが先程の「引数のオブジェクト」のケースです。

では、次はローカル変数で試してみましょう。

checkParams(Argument args) {
  final params = args.params;
  if (params is Params) {
    print('params: ${params.name}');
  }
}

void main() {
  final optionalParams = Params();
  final args = Argument(params: optionalParams);
  checkParams(args);
}

checkParamsにローカル変数(params)を定義して取り回しました。
すると同様に、flow analysisによって、non-nullableにpromotedされ、実行されまし

params: null

まとめ

Dartの型解析の仕様・動作を踏まえておくと、適切な形で型定義ができ、不用意な Non-Null Assertion Operator(!) を呼び出さなくて済みます。
不用意な Non-Null Assertion Operator はNullチェックのエラー(Null check operator used on a null value)に繋がりますので、できるだけ!は避けてコードを書いていきましょう!

Refs

Awarefy 技術ブログ

Discussion