Java, PHP習得者がDartを学ぶ 4

スマホアプリ開発を夢見てFlutter/Dartの勉強を真剣にはじめてみたバックエンドエンジニア歴十数年の筆者が躓いた点やプログラミングに関してあらためて理解を深めたこと、などを記した備忘録です。

nullセーフティ

Flutter/Dartを勉強し始めた頃、ネットで拾ってきたサンプルコードを実行してもコンパイルエラーが発生しまくるという事態によく見舞われました。
その原因の一つが今回の記事のテーマ「nullセーフティ」です。

nullセーフティとは「null値の参照型変数を参照しようとした時に発生するエラーを回避する仕組み」のことです。ここでいうエラーとは、JavaでおなじみNullPointerExceptionなどが該当します。

Dartでは、バージョン「2.12」からnullセーフティを利用することができ、バージョン3以降は完全にnullセーフティになるようです。2.12未満の場合、Flutterプロジェクト配下にあるパッケージ管理ファイル「pubspec.yaml」で有効化することもできます。
参考ページ:Dart公式(Sound null safety)

自分の開発環境の「pubspec.yaml」を確認してみると以下のようになっており、nullセーフティは有効な状態でした。

environment:
  sdk: '>=2.18.6 <3.0.0'

※pubspec.yamlについてや有効化については、同じくDart公式のこちらのページを参照

NullPointerExceptionは実行時エラーですが、nullセーフティの仕組みを利用すると、コンパイル時にnull参照エラーにつながりそうな箇所を検出してくれます。

nullセーフティの挙動確認

それではどのように検出してくれるのか、Dart公式の記述を確認しておきます。

nullセーフティ変数は、デフォルトで「non-nullable(null不可)」です。宣言された型の値のみを割り当てることができ、nullを割り当てることはできません。(例: int i = 42)
変数の型が nullable(null許容)であることを指定でき、その場合にのみ、nullまたは定義された型の値のいずれかを含めることができます。(例: int? i)

Dart公式(Sound null safety)

サンプルコードで見ていきましょう。

nullセーフティを使用すると、すべての変数がnon-nullable(null不可)になる。non-nullableの状態で変数宣言時、または利用時までに宣言された型で値が代入されない場合、変数利用箇所でコンパイルエラーとなる。

void main() {
    int i;
    String str = 'Dart';  
    print('$str $i'); // 変数i=nullのためコンパイルエラー
}

non-nullableな変数にnullを代入した場合、コンパイルエラーとなる。

void main() {
    int i;
    String str = 'Dart';  
    i = null; // 変数iにnullを代入しようとしたためコンパイルエラー
    print('$str $i');
}

nullを許容したい場合は、変数の型に”?”を付けて宣言する。

void main() {
    int? i;
    String? str;  
    i = null;
    str = null;
    print('$str $i'); // null null
}

nullでは無いことを保証する「!」

記号「!」は、nullableな型宣言の変数ではあるもののnullが入らないと確信できる状況で用い、non-nullableな型に変換するものです。

サンプルコードで見ていきましょう。  

変数priceがnon-nullableのint型、対してpriceMap[item]は、nullableのint型(int?)のため、コンパイルエラーとなる。

main() {
    Map<String, int> priceMap = {'りんご':150, 'みかん':80, 'キウイ':120};
    String item = 'りんご';
    int price = priceMap[item]; // コンパイルエラー(A value of type 'int?' can't be assigned to a variable of type 'int'.)
    print('$itemの値段は$price円です。');
}

変数の型に”?”を付けて宣言することでコンパイルエラーを回避できる。

main() {
    Map<String, int> priceMap = {'りんご':150, 'みかん':80, 'キウイ':120};
    String item = 'りんご';
    int? price = priceMap[item];
    print('$itemの値段は$price円です。'); // りんごの値段は150円です。
}

Mapに存在しないキーを指定して実行した場合、実行時エラーにはならず、price=nullのまま最後まで処理される。

main() {
    Map<String, int> priceMap = {'りんご':150, 'みかん':80, 'キウイ':120};
    String item = 'いちご';
    int? price = priceMap[item];
    print('$itemの値段は$price円です。'); // いちごの値段はnull円です。
}

priceMap[item]はnullが入らないものとし、「!」を付けてnon-nullableな型に変換することでコンパイルエラーを回避できる。

main() {
    Map<String, int> priceMap = {'りんご':150, 'みかん':80, 'キウイ':120};
    String item = 'りんご';
    int price = priceMap[item]!; // りんごの値段は150円です。
    print('$itemの値段は$price円です。');
}

priceはnon-nullableのため、Mapに存在しないキーを指定して実行した場合は実行時エラーとなる。

main() {
    Map<String, int> priceMap = {'りんご':150, 'みかん':80, 'キウイ':120};
    String item = 'いちご';
    int price = priceMap[item]!; // 実行時エラー
    print('$itemの値段は$price円です。');
}

上の例のような動作の違いから、コンパイルエラー回避で安易に「?」を使い、nullのまま処理させるより、コーディングミス以外でnullが入らないと確信できる場合は「!」を使ったほうがよさそうです。想定外のnullは実行時エラーで処理したほうがデバッグが容易になるからです。

null-aware演算子

変数の値がnullだった場合のみ代わりの値を設定できる演算子「??=」、「??」が用意されています。いずれもnull参照エラーを防ぐために使用します。

演算子「??=」  

変数がnullの場合のみ右辺値を代入する。

main() {
      int? num;
      num ??= 10;
      print(num); // 10
      num ??= 100;
      print(num); // 10
  }

演算子「??」

代入する変数がnullの場合のみ右辺値を代入する。

main() {
      int? num1;
      int? num2;
  
      num1 = 100;
      num2 = num1 ?? 10;
      print(num2); // 100
  
      num1 = null;
      num2 = num1 ?? 10;
      print(num2); // 10    
  }

※サンプルコードは「Flutter 3.7.6 Dart SDK 2.19.3」で実行しました。