【Dart】Null Safety な書き方チートシート
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? str = "dog";
// strがnullではないことが確定しているため、`?`が外れたString型として扱える
str.contains("dog");
// nullを代入するとString?型に戻る
str = null;
str.contains("dog"); // エラー
// nullの可能性がある場合は `?.`を使う(詳しくは後述)
str?.contains("dog");
?
を外す方法
変数からString?
をString
にするような、変数の型から?
を外した状態で扱う方法はいくつかあります。
??
を使う
?
付きの変数の後に??
を付けると、その変数がnull
だった場合は??
以降の値が使われます。
(str ?? ""); // strがnullだったら "" になる
(integer ?? 0); // integerがnullだったら 0 になる
String? str;
// strはnullなので str?.contains と書かないとエラーになる
str.contains("dog"); // エラー
// strがnullだと "dog cat" になるため、`?`の無いString型として扱える
(str ?? "dog cat").contains("dog"); // true
!
を使う
!
を使うと一時的に変数から?
を外せますが、null
だった場合はアプリが落ちたりエラーが発生したりします。
Null Safety はnull
でもアプリが落ちない設計にするとともに、バグの特定をしやすくするためのものなので、これでは意味がありません…。
無闇に使わない方が良いです。
基本的に絶対にnull
にならない状況や、null
だった場合はtry catch
で別の処理をさせたい時に用います。
String? str;
str = "dog";
/* 何か色々処理 */
// strに値が入っている場合はString型として扱われ、nullだった場合はエラーになる
str!.contains("dog");
一度 `!` を使うと同じ関数などの範囲(スコープ)では `?` を外せる
str.contains("dog");
String? str;
try {
// エラーが発生すると下のcatch内の処理が行われるためアプリが落ちなくなる
str!.contains("dog");
} catch (e) {
print(e.toString());
}
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;
str = "dog cat";
/* 何か色々処理 */
// 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")) {
// 何か処理
}
?
の外せない変数
String? str;
void main() {
str = "dog";
// nullかどうか確認しても `?` か `!` を付ける必要がある
if(str != null) {
str!.contains("dog");
}
str!.contains("dog");
// 2回目以降も ! が必須
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;
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;
sample = SampleClass();
/* 何か色々処理 */
if(sample?.text != null) {
sample.text.contains("dog"); // エラー
}
}
こう書くと外せます。
class SampleClass {
final String text = "dog cat";
}
void main () {
SampleClass? sample;
sample = SampleClass();
/* 何か色々処理 */
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]
備考
本記事は以下の環境で検証しました
Dart SDK version: 3.4.4 (stable) (Wed Jun 12 15:54:31 2024 +0000) on "macos_arm64"
Discussion