【Dart】Nullの扱い方について

こんにちは。エキサイト株式会社でエンジニアをしている新卒の岡島です。 普段業務ではFlutterを用いたアプリ開発を行っています。 今回は、業務中にnullの扱いについて悩む場面があったので、Dartのドキュメントを読んで良いとされるnullの扱い方をみていこうと思います。

nullとは

nullとは、何のデータも含まれない状態のことです。nullを不適切に扱うとクラッシュするなどバグの原因となることもあるので注意が必要です。

DartにはNull Safetyと呼ばれるnullを健全に扱う機能が備わっています。これにより、Dartはデフォルトでnullを許容せず、nullに関するエラーはコンパイル時に検出されるようになりました。

今回、この記事ではDart公式ドキュメントの中のEffective Dartで触れられているnullについての扱い方を見ていきます。

Effective Dartで推奨されているnullの扱い方

Dartのドキュメントで触れられているnullの扱い方について調べてみました。以下に、具体的なポイントをまとめます。

環境

Dart version: 3.4

明示的にnullを初期化しない

Dartでは変数がNull許容型の場合、変数が暗黙的にnullで初期化されるようになっています。

良いとされるコード

Item? bestItem;

void error([String? message]) {
  stderr.write(message ?? '\n');
}

悪いとされるコード

Item? bestItem = null;

void error([String? message = null]) {
  stderr.write(message ?? '\n');
}

Null許容型はデフォルトでnullとして初期化されるのでわざわざnullで明示的に初期化する必要はないということです。これは関数のオプション引数の場合も同様です。

Null許容型の等価演算では true または false を使用しない

Null許容型のブール式を評価するには、??!= nullを用いて、nullチェックをする必要があります。

良いとされるコード

// nullの場合はfalseにする
if (nullableBool ?? false) { ... }

// nullの場合はfalseにし、変数を型昇格させる
if (nullableBool != null && nullableBool) { ... }

悪いとされるコード

// nullの場合はエラーになる
if (nullableBool) { ... }

// nullの場合はfalseになる
if (nullableBool == true) { ... }

悪いとされるコードの例であるif (nullableBool) { ... } では、コードがnullと関係があることを示していないため、null非許容型と間違える可能性があります。また、nullableBoolがnullの場合、if(nullableBool)はfalseとして評価されるため、if (nullableBool ?? false) { ... }のように明示的にnullの場合はfalseとして扱うことが好ましいとされています。

ただ、条件式の中の変数に対して??を使用しても、Null非許容型に昇格されないため、?? の代わりに明示的な != null チェックを使用することが推奨されています。

変数が初期化されているかどうかを確認する必要がある場合は、late変数を避ける

Dartでは、late変数が初期化されているかどうかを確認する方法がありません。late変数に状態を格納し、変数が設定されたかどうかを追跡する別のbool値フィールドを持つことで、初期化を検出できますが、Dartは内部的にlate変数の初期化状態を保持するため冗長です。通常は変数をNull許容型にする方が明確です。この場合、nullをチェックすることで変数が初期化されたかどうかを確認できます。

Null許容型を使用するための型昇格またはnullチェックを検討する

Null許容の変数がnullではないことを確認すると、その変数はNull非許容型に昇格されます。 型昇格がサポートされているのは以下の場合です。

  • ローカル変数
  • パラメーター
  • プライベートなfinalフィールド

nullチェックパターンの使用

class UploadException {
  final Response? response;

  UploadException([this.response]);

  @override
  String toString() {
    if (this.response case var response?) {
      return 'Could not complete upload to ${response.url} '
          '(error code ${response.errorCode}): ${response.reason}.';
    }
    return 'Could not upload (no response).';
  }
}

こちらの例ではnullチェックパターンを使用することで、メンバーの値が null ではないことが同時に確認され、その値が同じ基本型の新しい Null 非許容変数にバインドされます。

ローカル変数を使用する

class UploadException {
  final Response? response;

  UploadException([this.response]);

  @override
  String toString() {
    final response = this.response;
    if (response != null) {
      return 'Could not complete upload to ${response.url} '
          '(error code ${response.errorCode}): ${response.reason}.';
    }
    return 'Could not upload (no response).';
  }
}

この例ではthis.responseをローカル変数responseに割り当て、その変数に対してnullチェックを行っています。これにより、responseをNull非許容型として安全に扱うことができます。

最後に

nullの扱いはコードの安全性と信頼性を向上させるために知っておく必要があると思い調べてみました。nullチェックや型昇格などにより、安全で読みやすいコードを意識して今後も業務に取り組みたいと思います。