🐈

【Dart】Null Safety な書き方チートシート

2023/03/30に公開

Dartの公式に Null Safety の解説ページ は当然あるのですが、Dartの経験者向けの内容ですし、一覧で確認できる記事があっても良いのではと思い書きます。

nullを許容する変数の書き方

nullが入る可能性のある変数を定義する時は?を付けます。

String? str = null;
int? integer = null;

// ? 付きの変数に何も初期値を入れない場合、自動でnullが入る
// なので以下の書き方は上と同じ意味

String? str;
int? integer;

?を付けないとnullが入れられずエラーになります。

String str = null; // エラー
int integer = null; // エラー

String str; // エラー
int integer; // エラー

変数から?を外す方法

String?Stringにするような、変数の型から?を外す方法はいくつもあります。
ただし、基本的に同じ関数内など、決まった範囲(スコープ)でのみ効果があり、そこから外に出るとまた?付きの変数に戻ります。

!を使う

!を使うと一時的に変数から?を外せますが、nullだった場合はアプリが落ちたりエラーが発生したりします。

Null Safety はnullでもアプリが落ちない設計にするとともに、バグの特定をしやすくするためのものなので、これでは意味がありません…。
無闇に使わない方が良いです。

基本的に絶対にnullにならない状況や、nullだった場合はtry catchで別の処理をさせたい時に用います。

String? str;

str = "dog cat";

// すぐ上で代入しているため ! を消してもエラーにならないが
// 上の代入を消したり、コードが複雑になったりすると ! を付けないとエラーになる
str!.contains("dog");
// 一度 ! を使うと同じ範囲(スコープ)であればStringとして扱えるため、以降は ! を外せるようになる
// 同じ範囲とは、大抵は同じ {} の中のこと(if文やfor文、関数など)
str.contains("dog");

str = null; // nullを代入すると、また ? 付きに戻る
String? str;

try {
    // エラーが発生すると下のcatch内の処理が行われるためアプリが落ちなくなる
    //
    // try内のコードが長くなるとバグの特定に時間がかかるため、
    // その場合は ! ではなくif文などでの確認がおすすめ
    str!.contains("dog");
} catch (e) {
    print(e.toString());
}

??を使う

?付きの変数の後に??を付けると、その変数がnullだった場合は??以降の値が使われます。

(str ?? ""); // strがnullだったら "" になる
(integer ?? 0); // integerがnullだったら 0 になる
String? str;

// strがnullだと "dog cat" になるため、これはtrue
(str ?? "dog cat").contains("dog");

// ?? で ? が外せる範囲は限定的なので、すぐ下の行であってもエラーになる
str.contains("dog"); // エラー

if文を使う

if文でnullではないと確認した場合は?を外せます。

String? str;

str = "dog cat";

if(str != null) {
    str.contains("dog"); // if文で確認後はStringとして扱える
}

if(str == null) {
    // 何か処理
} else {
    str.contains("dog"); // これもnullかどうか確認しているのでStringとして扱える
}

// 三項演算子もif文と同様。これは先ほどの `(str ?? "")` と同じ意味
(str != null ? str : "");

if(str == null) return;
str.contains("dog"); // nullならreturnによってここまで到達しないのでStringとして扱える

同じif文の&&||にも効果があります。

String? str;

// nullだった場合は `str != null` がfalseになり、
// && 以降は飛ばされるので ? を外せる(Stringとして扱える)
if(str != null && str.contains("dog")) {
    // 何か処理
}

// strがnullまたは "dog" という文字列を含んでいるか確認するif文
//
// nullだった場合は `str == null` がtrueになり、
// || 以降は飛ばされるので ? を外せる(Stringとして扱える)
if(str == null || str.contains("dog")) {
    // 何か処理
}

ただしグローバル変数のように様々な場所から代入できる変数の場合、if文で確認してから使うまでの間にnullが入る可能性があるので !を外せません。

lateを使う

上述した通り、絶対にnullにならない状況では!を使っても良いのですが、毎回!を使うのも面倒な場合はlateで変数を宣言します。

'late'という英単語の通り、遅れて値を入れる変数という意味です。
lateで宣言した変数を、何も値を入れていないうちに使おうとするとエラーになります。

late String str1;

str1.length; // まだ値が入っていないのでエラー

late String str2;

str2 = "dog cat";

str2.length; // 値を入れた後なのでエラーにならない

lateは、むしろエラーを起こすことでバグを検知するという使い方も出来ます

class Dog {
    // これでも良いけど、"" のままで使うことはない(もしあったらバグ)
    var name = "";

    Dog() {
        // 将来このコードを消すことになった際、
        // うっかり値を入れ忘れて "" のままにしてしまうかもしれない…
        name = "Pochi";
    }
}
class Dog {
    // "" の代わりにlateを使用
    late String name;

    Dog() {
        // 将来このコードを消すことになった際、
        // うっかり値を入れ忘れたらエラーが起きるのですぐに分かる
        name = "Pochi";
    }
}

late finalにすると、2回目以降の代入はエラーになります。

class Dog {
    late final String name;

    Dog() {
        name = "Pochi";
        name = "Tama"; // 2回目なのでエラー
    }
}

?付きのまま変数を扱う

?を外さずに変数を扱う場合は?を付けます。

String? str = null;

str?.contains("dog"); // ? を外していないので、bool?型が返る。strがnullの場合はnullが返る

// nullの可能性があるのでエラー
if(str?.contains("dog")) {
    // 何か処理
}

// strがnullの場合はfalseになるようにすればエラーにならない
if(str?.contains("dog") ?? false) {
    // 何か処理
}

引数の型が?付きだったり、?付きの型に代入する場合は?を付けなくても構いません。

String? str1;
String str2 = str1; // str2は ? 付きではないのでエラー
String? str1;
String? str2 = str1; // str2は ? 付きなのでエラーにならない

Mapの Null Safety

Mapの変数から値を取り出す時は、基本的に?付きになります。

final map = {"a": 1};

map["a"]; // 入っている値はint型の 1 だが、int?型になる
map["b"]; // "b" は入っていないためnullになる
Map<String, int>? map;

map["a"]!; // ! を付けてもmap自体が ? 付きなので、これはint?型
map!["a"]!; // ! を両方に付けるとint型になり ? を外せる
(map["a"] ?? 0) // ?? だと両方の ? を一気に外せる

クラス内で宣言する変数の Null Safety

// 初期化する場合は ? を付けずに宣言できる
class SampleClass {
    String text = "dog cat";
}
// 初期化していないのでエラー
class SampleClass {
    String text;
}
// コンストラクター(this付きの引数)で初期化する場合
class SampleClass {
    String text;

    SampleClass(this.text);
}

void main() {
    final sample = SampleClass("dog cat");
    print(sample.text); // dog cat
}
// コンストラクター(初期化子)で初期化する場合
class SampleClass {
    String text;

    SampleClass(String text): text = text;
}

void main() {
    final sample = SampleClass("dog cat");
    print(sample.text); // dog cat
}
// これはエラーになる
// コンストラクターで初期化する場合、
// thisか初期化子以外の方法だとlateか ? を付ける必要がある
class SampleClass {
    String text;

    SampleClass() {
        text = "dog cat";
    }
}
// 先ほどのエラーになる書き方も含め、? を付けると初期化しなくても基本的にエラーにならない
// (finalを付けて宣言した場合を除く)
class SampleClass {
    String? text;
}

?付きで宣言したクラスのオブジェクトの Null Safety

class SampleClass {
    String text = "dog cat";
}

void main () {
    SampleClass? sample;

    sample?.text; // 元はStringなものの、sampleが ? 付きなのでString?になる
    sample?.text ?? ""; // sampleがnullだと "" になる
    sample!.text; // sampleがnullだとエラーになる
}

?が外せそうで外せない書き方

Dartの Null Safety はまだ完全ではないようで、いかにも?が外せそうでも外せない場合があります。

以下はif文で確認しているので外せそうに見えますが外せません。

class SampleClass {
    final String text = "dog cat";
}

void main () {
    SampleClass? sample;

    if(sample?.text != null) {
        sample!.text.contains("dog");
    }
}

こう書くと外せます。

class SampleClass {
    final String text = "dog cat";
}

void main () {
    SampleClass? sample;

    if(sample != null) {
        sample.text.contains("dog");
    }
}

クラス内の?付きの変数の場合、if文で確認しても?を外すことが出来ません。
!を使うなり、何か他の変数に入れるなりする必要があります。

class SampleClass {
    final String? text = "dog cat"; // finalなのでnullになることはない
}

void main () {
    final sample = SampleClass();

    if(sample.text != null) {
        sample.text!.contains("dog"); // ! か ? を付けないとエラーになる
    }
}

??=nullの場合だけ代入

??=を使うと、nullだった場合にだけ代入を行います。

String? str;

str ??= "dog cat"; // strはnullなので "dog cat" が入る
String? str = "";

str ??= "dog cat"; // strはnullではないので "dog cat" が入らず、"" のまま

主に値が配列のMapを初期化する際に用います。

final map = <String, List<int>>{};

map["a"]?.add(100); // map["a"] がnullなので値を入れられない
print(map["a"]); // null

map["a"] ??= []; // 初期化。すでに配列が入っている場合は実行されないので安全
map["a"]!.add(100);
print(map["a"]); // [100]

// 少し読みづらいがMapから "b" を探す処理が一度だけになるので効率的
(map["b"] ??= []).add(200);
print(map["b"]); // [200]

// putIfAbsentでも同じ処理が行える
map.putIfAbsent("c", () => []).add(300);
print(map["c"]); // [300]

他にも「重い処理を一度だけ行える」ので、プログラムの動作の高速化などに使えます。

前回書いた記事のサンプルコードでも一箇所使っています(??=でページ内検索すると見つかります)
もしここで??=を使わなかった場合、for文が一周する度に重い処理が行われてしまいます。

備考

本記事は以下の環境で検証しました

Dart SDK version: 2.19.5 (stable) (Mon Mar 20 17:09:37 2023 +0000) on "macos_arm64"
GitHubで編集を提案
合同会社zoome(ズーム)

Discussion