🔍

【Dart】! に頼らないnullチェック ~安全な書き方&if-case文活用法~

2025/03/12に公開
2

はじめに

皆さん、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);
  }
}
/// switch文
void someFunc8(Todo? todo) {
  // todoのswitch文
  switch (todo) {
    case null:
      print('null');
    case Todo(title: final title, content: final content)
        when title != null && content != null:
      nonNullValuePrint(title);
      nonNullValuePrint(content);
  }
  // titleのswitch文
  switch (todo?.title) {
    case null:
      print('null');
    case final title:
      nonNullValuePrint(title);
  }
}
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);
    someFunc8(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 1todo自体のnullチェックがされていないのでこのままだとエラーになります。

bad2 todo?.content != null のチェックでは todo.content がnullでないことを保証できません。
そのため、todo.content! を使おうとすると コンパイルエラー になります。
todo?.content! にすると ?.! の意味が矛盾するためエラー)

good 1 は両方のnullチェックを && で繋げた形です。

good 2 は先ほどのべた変数に代入することで安全にnullチェックができています。

good 3good 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文とはちょっとだけ変更点がありますね。
日本語で読んでいくとすると、 todoTodo として存在していた場合にその中の値を一旦 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);
  }
}

この場合だとちょっと長いですね😅
ただ、この書き方は省略できるのでその点は後述します。

switch文でnullの時もハンドリングする

void someFunc8(Todo? todo) {
  // todoのswitch文
  switch (todo) {
    case null:
      print('null');
    case Todo(title: final title, content: final content)
        when title != null && content != null:
      nonNullValuePrint(title);
      nonNullValuePrint(content);
  }
  // titleのswitch文
  switch (todo?.title) {
    case null:
      print('null');
    case final title:
      nonNullValuePrint(title);
  }
}

if-case文はいわゆるパターンマッチングなので、switch文にも適用できます。

whenの省略と応用

ここからはif-case文の書き方に慣れてきたら以下のようなこともできますよ!という例を載せておこうと思います。

test/example2_test.dart
// ignore_for_file: avoid_print

import 'package:flutter_test/flutter_test.dart';

class Memo {
  Memo({
    this.id,
    this.content,
    this.isSecret,
    this.tags,
    this.dynamicValue,
  });

  final int? id;
  final String? content;
  final bool? isSecret;
  final List<String>? tags;
  final dynamic dynamicValue;
}

void nonNullValuePrint(Object value) {
  print(value);
}

void someFunc1(String? value) {
  // 通常のif文
  if (value != null) {
    nonNullValuePrint(value);
  }
  // if-case文
  if (value case final nonNullValue when nonNullValue != null) {
    nonNullValuePrint(nonNullValue);
  }

  // if-case文のwhenを省略するnullチェック
  if (value case final nonNullValue?) {
    nonNullValuePrint(nonNullValue);
  }
}

void someFunc2(Memo memo) {
  // if-case文
  if (memo.content case final content when content != null) {
    nonNullValuePrint(content);
  }

  // if-case文のwhenを省略するnullチェック
  if (memo.content case final content?) {
    nonNullValuePrint(content);
  }
}

void someFunc3(Memo memo) {
// 通常のif文
  final tags = memo.tags;
  if (tags != null && tags.isNotEmpty) {
    nonNullValuePrint(tags);
  }

  // if-case文でwhenの条件を追加する
  if (memo.tags case final tags? when tags.isNotEmpty) {
    nonNullValuePrint(tags);
  } else if (memo.tags case final tags? when tags.isEmpty) {
    nonNullValuePrint(tags);
  } else {
    print('null');
  }

  // if-case文でwhenの条件を追加する
  if (memo.id case final id? when id < 50) {
    nonNullValuePrint(id);
  } else if (memo.id case final id? when id.isNegative) {
    nonNullValuePrint(id);
  } else {
    print('null');
  }
}

void someFunc4(Memo? memo) {
  // if-case文でwhenの条件を追加する
  if (memo case Memo(id: final id?) when id <= 50) {
    nonNullValuePrint(id);
  } else if (memo case Memo(isSecret: final isSecret?) when isSecret == true) {
    nonNullValuePrint(isSecret);
  } else if (memo case Memo(tags: final tags?) when tags.contains('tag')) {
    nonNullValuePrint(tags);
  } else if (memo case Memo(content: final title) when title == null) {
    print('null');
  }
}

void someFunc5(Memo memo) {
  // if-case文でwhenの条件を追加する
  if (memo.dynamicValue case final value? when value is String) {
    nonNullValuePrint(value);
  } else if (memo.dynamicValue case final value? when value is int) {
    nonNullValuePrint(value);
  } else {
    print('null');
  }
}

void main() {
  final memo = Memo(
    id: 1,
    content: 'content',
    isSecret: false,
    tags: ['tag1', 'tag2'],
    dynamicValue: 'dynamicValue',
  );

  final memo2 = Memo(
    id: 1,
    content: 'content',
    isSecret: true,
    tags: ['tag1', 'tag2'],
    dynamicValue: 1,
  );
  test('test_name', () {
    someFunc1('value');
    someFunc1(null);
    someFunc2(memo);
    someFunc3(memo);
    someFunc4(memo2);
    someFunc5(memo);
    someFunc5(memo2);
  });
}

前提条件

先ほどとはちょっとだけオブジェクトの中身を変えたMemoを使います。

class Memo {
  Memo({
    this.id,
    this.content,
    this.isSecret,
    this.tags,
    this.dynamicValue,
  });

  final int? id;
  final String? content;
  final bool? isSecret;
  final List<String>? tags;
  final dynamic dynamicValue;
}

void nonNullValuePrint(Object value) {
  print(value);
}

when value != nullを省略する

実は先ほどから書いていた、nullチェックの部分は省略形が存在します。

void someFunc1(String? value) {
  // 通常のif文
  if (value != null) {
    nonNullValuePrint(value);
  }
  // if-case文
  if (value case final nonNullValue when nonNullValue != null) {
    nonNullValuePrint(nonNullValue);
  }

  // if-case文のwhenを省略するnullチェック
  if (value case final nonNullValue?) {
    nonNullValuePrint(nonNullValue);
  }
}

だいぶスッキリしましたね。
? で省略した形です。ただ、最初の頃はこれだとnullを剥がせていないのでは?と思ってしまいがち。
ただ、ちゃんとコンパイルは通るし、変数にカーソルを当てると null は剥がせています。

日本語に無理やりするならば

もし、valueがnullの可能性のあった(?)nonNullValueをnullではない値に固定(final)できるならば...

と言った感じでしょうか。(だいぶ無理やり)

オブジェクト内のnullableな値を取り出す

この場合ももちろんそのまま適用できます。

void someFunc2(Memo memo) {
  // if-case文
  if (memo.content case final content when content != null) {
    nonNullValuePrint(content);
  }

  // if-case文のwhenを省略するnullチェック
  if (memo.content case final content?) {
    nonNullValuePrint(content);
  }
}

whenで様々な条件を追加する

whenは本来、検査対象を追加するキーワードです。
よって当然 null だけではなく様々条件を入れることができます。

void someFunc3(Memo memo) {
// 通常のif文
  final tags = memo.tags;
  if (tags != null && tags.isNotEmpty) {
    nonNullValuePrint(tags);
  }

  // if-case文でwhenの条件を追加する
  if (memo.tags case final tags? when tags.isNotEmpty) {
    nonNullValuePrint(tags);
  } else if (memo.tags case final tags? when tags.isEmpty) {
    nonNullValuePrint(tags);
  } else {
    print('null');
  }

  // if-case文でwhenの条件を追加する
  if (memo.id case final id? when id < 50) {
    nonNullValuePrint(id);
  } else if (memo.id case final id? when id.isNegative) {
    nonNullValuePrint(id);
  } else {
    print('null');
  }
}

void someFunc4(Memo? memo) {
  // if-case文でwhenの条件を追加する
  if (memo case Memo(id: final id?) when id <= 50) {
    nonNullValuePrint(id);
  } else if (memo case Memo(isSecret: final isSecret?) when isSecret == true) {
    nonNullValuePrint(isSecret);
  } else if (memo case Memo(tags: final tags?) when tags.contains('tag')) {
    nonNullValuePrint(tags);
  } else if (memo case Memo(content: final title) when title == null) {
    print('null');
  }
}

条件に型を追加する場合は次のようになります。

void someFunc5(Memo memo) {
  // if-case文でwhenの条件を追加する
  if (memo.dynamicValue case final value? when value is String) {
    nonNullValuePrint(value);
  } else if (memo.dynamicValue case final value? when value is int) {
    nonNullValuePrint(value);
  } else {
    print('null');
  }
}

終わりに

いかがだったでしょうか?
今回はnullチェックの基本を抑えつつ、if-case文によるnullチェックの方法をご紹介しました。
最初のうちは無理にif-case文を使わずに変数に代入するなどの方法を使って、
できるだけ ?! を使わない記述を心がけていければいいかなと考えています。

if-case文は個人的には気に入っているものの、ここは最終的には個人の理解度やチームの方針によると思います。
まずは皆さんもDartPadやテストファイルで試してみて、自分に合った方法を模索してみてはいかがでしょうか?

この記事が誰かのお役に立てれば幸いです。

参考記事

https://zenn.dev/aria3/articles/ddbdbbb65b472f

Discussion

aqaq

私も最近知った書き方なので恐縮ですが、以下の部分はwhen以降を?に省略できますね。

if (value case final someValue when someValue != null)
if (value case final someValue?)
はるさんMobile.Junior.EngineerはるさんMobile.Junior.Engineer

コメントありがとうございます!
先ほど手元で確認しました!
ほんとですね😳
whenの方が可読性はいいですが、慣れるとこちらの方が短いのでいいですね!
意味として理解できないでもないので。
final someValueとするけど、これは本来null(?)の可能性があるものですよー、って感じでしょうか?
であればfinal someValueの時点でそうしてくれればいいのにとは思いますが😇

いずれにせよ、ありがとうございます!勉強になりました。
記事もできるだけ早く更新しようと思います🙏