はじめに
※ 本記事の内容は以下で掲載した内容と同じになります。
皆さん、nullチェック、ちゃんとできていますか?
「この値、nullじゃないはず…」と !
をつけたら Null check operator used on a null value でクラッシュ!
そんな経験、ありますよね?(筆者はあります)
nullを適切に扱えないと、バグの温床 になるだけでなく、
「!
をつけたけど、これ本当に大丈夫?」という 不安 を抱えながらコードを書くことになります。
本記事では、Dartの基本的なnullチェックから if-case
を活用した !
なしの安全な書き方 までを解説していきます。
記事の対象者
- 「Dartのnullチェック、結局どう書くのがベスト?」 と迷っている方
!
を使わずに、安全&スマートにnullを扱いたい方if-case
を使って、もっとエレガントなコードを書きたい方- 「nullチェックでバグを踏みたくない!」と本気で思っている方
記事を執筆時点での筆者の環境
[✓] Flutter (Channel stable, 3.27.1, on macOS 15.1 24B2082 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] VS Code (version 1.96.2)
ソースコード全体
test/example_test.dart
// ignore_for_file: avoid_print
import 'package:flutter_test/flutter_test.dart';
class Todo {
Todo({
this.title,
this.content,
});
final String? title;
final String? content;
}
void nonNullValuePrint(String stg) {
print(stg);
}
void someFunc(String? value) {
if (value != null) {
nonNullValuePrint(value);
}
}
void someFunc2(Todo todo) {
// 強制アンラップ
if (todo.title != null) {
nonNullValuePrint(todo.title!);
}
// 変数に代入
final title = todo.title;
if (title != null) {
nonNullValuePrint(title);
}
// if-case文
if (todo.title case final title when title != null) {
nonNullValuePrint(title);
}
}
void someFunc3(Todo? todo) {
// bad 1 エラー
// if (todo.content != null) {
// nonNullValuePrint(todo.content);
// }
// bad 2 エラー
// if (todo?.content != null) {
// nonNullValuePrint(todo.content!);
// }
// good 1
if (todo != null && todo.content != null) {
nonNullValuePrint(todo.content!);
}
// good 2
final title = todo?.title;
if (title != null) {
nonNullValuePrint(title);
}
// good 3
if (todo != null) {
final content = todo.content;
if (content != null) {
nonNullValuePrint(content);
}
}
}
void someFunc4(Todo? todo) {
// 早期リターン
if (todo == null) return;
final content = todo.content;
if (content == null) return;
nonNullValuePrint(content);
}
void someFunc5(Todo? todo) {
if (todo case Todo(content: final content) when content != null) {
nonNullValuePrint(content);
}
}
/// 複数の値を取り出す場合 通常
void someFunc6(Todo? todo) {
if (todo == null) return;
final title = todo.title;
final content = todo.content;
if (title != null && content != null) {
nonNullValuePrint(title);
nonNullValuePrint(content);
}
}
/// 複数の値を取り出す場合 if-case文
void someFunc7(Todo? todo) {
if (todo case Todo(title: final title, content: final content)
when title != null && content != null) {
nonNullValuePrint(title);
nonNullValuePrint(content);
}
}
void main() {
final todo = Todo(title: 'title', content: 'content');
test('test_name', () {
someFunc('value');
someFunc2(todo);
someFunc3(todo);
someFunc4(todo);
someFunc5(todo);
someFunc6(todo);
someFunc7(todo);
});
}
前提条件
今回は以下のオブジェクトとメソッドを使って解説していきます。
class Todo {
Todo({
this.title,
this.content,
});
final String? title;
final String? content;
}
void nonNullValuePrint(String stg) {
print(stg);
}
Todo
にはnullableなパラメータが2つありあます。
nonNullValuePrint
メソッドの引数は非null許容です。
if (value != null) でnullチェック
まず、基本的なnullチェックとしては以下での方法でしょう。
対象の値がnullかどうかをif文で検証します。
void someFunc(String? value) {
if (value != null) {
nonNullValuePrint(value);
}
}
オブジェクト内の値をnullチェック
次に先ほどの前提条件で述べたオブジェクトをつかった例をみていきます。
以下に3パターン用意しました。
void someFunc2(Todo todo) {
// nullチェック + !
if (todo.title != null) {
nonNullValuePrint(todo.title!);
}
// 変数に代入
final title = todo.title;
if (title != null) {
nonNullValuePrint(title);
}
// if-case文
if (todo.title case final title when title != null) {
nonNullValuePrint(title);
}
}
まず、引数であるtodo
は非null許容です。
しかし、その中身の title
はnullableな値です。
nullチェック + !
最初に todo.title != null
で検証しているはずなのにメソッドの引数には todo.title!
として !
(エクスクラメーションマーク)をつけています。
学習し始めた頃は 「なぜ?チェックしたのに!!」 っとなったのものです。
こちらは一応最初に検証しているので、間違いなくnullではないことは保証されますが、 !
を使っているので個人的にはみた時に一瞬ハッとしてしまいます。
しかし、次のようにするとnullを完全に剥がすことができます。
変数に代入する
2つ目の if
文では、一度変数に代入した値を null チェックすることで、最終的に !
をつけなくてもよくなります。
これは、Dart の型システムの仕様によるものです。
Dart では、クラスのフィールド (インスタンス変数) は if
の中で null チェックしても、必ずしも null でないとは保証されません。
そのため、todo.title!
のように !
をつけないと、コンパイルエラーになります。
一方で、ローカル変数 title
に代入すると、Dart はそのスコープ内で値が変わらないと判断できるため、null でないことを保証できます。
これが 「非nullローカル変数のプロモーション」 (null-safe type promotion) という仕組みです。
まとめると、オブジェクトの nullable なフィールドは、一度ローカル変数に代入すると安全!ということですね。
if-case文
三つ目のものが近年Dart3.0で追加されたパターンマッチ構文を使った書き方です。
最初のうちは見慣れない構文なので、特に初学者の方は読みづらいかもしれません。
しかし、慣れてくるとこれが一つ目のnullチェックと二つ目のnullチェックの合わせ技であることがわかります。
日本語にちょっと無理やりすると以下のような形でしょうか。
// もし、 `value` が `someValue` という値として固定(`final`)され、
// `someValue` が `null` ではない 場合(when) というパターン(case)だとしたら...
if (value case final someValue when someValue != null)
一般的にはif-case構文と呼ばれていますが、私としてはif-case-final-when構文として覚えた方が良さそうです。
引数もnullableだった場合
通常のnullチェック
void someFunc3(Todo? todo) {
// bad 1 エラー
// if (todo.content != null) {
// nonNullValuePrint(todo.content);
// }
// bad 2 エラー
// if (todo?.content != null) {
// nonNullValuePrint(todo.content!);
// }
// good 1
if (todo != null && todo.content != null) {
nonNullValuePrint(todo.content!);
}
// good 2
final title = todo?.title;
if (title != null) {
nonNullValuePrint(title);
}
// good 3
if (todo != null) {
final content = todo.content;
if (content != null) {
nonNullValuePrint(content);
}
}
}
bad 1 はtodo
自体のnullチェックがされていないのでこのままだとエラーになります。
bad2 todo?.content != null
のチェックでは todo.content
がnullでないことを保証できません。
そのため、todo.content!
を使おうとすると コンパイルエラー になります。
(todo?.content!
にすると ?.
と !
の意味が矛盾するためエラー)
good 1 は両方のnullチェックを &&
で繋げた形です。
good 2 は先ほどのべた変数に代入することで安全にnullチェックができています。
good 3 は good 1 をネストして書いた形ですね。
早期リターン
void someFunc4(Todo? todo) {
// 早期リターン
if (todo == null) return;
final content = todo.content;
if (content == null) return;
nonNullValuePrint(content);
}
上記のようにnullだったら処理を終わらせてしまうパターンも考えられます。
ただし、これは実装内容によってはエラーハンドリングできなくなってしまうので、注意は必要です。
if-case文
void someFunc5(Todo? todo) {
if (todo case Todo(content: final content) when content != null) {
nonNullValuePrint(content);
}
}
先ほど見たif-case文とはちょっとだけ変更点がありますね。
日本語で読んでいくとすると、 todo
が Todo
として存在していた場合にその中の値を一旦 final content
として、
その content
がnullではなかった場合...
という文脈で読めます。
複数の値をnullチェックして取り出す場合
通常のnullチェック
/// 複数の値を取り出す場合 通常
void someFunc6(Todo? todo) {
if (todo == null) return;
final title = todo.title;
final content = todo.content;
if (title != null && content != null) {
nonNullValuePrint(title);
nonNullValuePrint(content);
}
}
if-case文
/// 複数の値を取り出す場合 if-case文
void someFunc7(Todo? todo) {
if (todo case Todo(title: final title, content: final content)
when title != null && content != null) {
nonNullValuePrint(title);
nonNullValuePrint(content);
}
}
この場合だとちょっと長いですね😅
個人的には hoge.value
よりは value
として扱いたいので、こちらが好きですが
ここまでくると好みかもしれません。
終わりに
いかがだったでしょうか?
今回はnullチェックの基本を抑えつつ、if-case文によるnullチェックの方法をご紹介しました。
最初のうちは無理にif-case文を使わずに変数に代入するなどの方法を使って、
できるだけ ?
や !
を使わない記述を心がけていければいいかなと考えています。
if-case文は個人的には気に入っているものの、ここは最終的には個人の理解度やチームの方針によると思います。
まずは皆さんもDartPadやテストファイルで試してみて、自分に合った方法を模索してみてはいかがでしょうか?
この記事が誰かのお役に立てれば幸いです。
参考記事