メインコンテンツにスキップ

Null safety: よくある質問

このページでは、Google の内部コードの移行経験に基づいた、null safety に関するよくある質問をいくつかまとめています。

移行されたコードのユーザーが認識しておくべき実行時の変更点は何ですか?

#

移行による影響のほとんどは、移行されたコードのユーザーにすぐには影響しません。

  • ユーザーに対する静的な null safety チェックは、コードを移行したときに初めて適用されます。
  • 完全な null safety チェックは、すべてのコードが移行され、サウンドモードがオンになったときに発生します。

注意すべき例外が2つあります。

  • ! 演算子は、すべてのモード、すべてのユーザーにとって実行時の null チェックです。したがって、移行時には、呼び出し元のコードがまだ移行されていなくても、その場所に null が流れることがエラーとなる場合にのみ ! を追加するようにしてください。
  • late キーワードに関連する実行時チェックは、すべてのモード、すべてのユーザーに適用されます。フィールドに late をマークするのは、使用される前に必ず初期化されることがわかっている場合のみにしてください。

テストでのみ null になる値はどうなりますか?

#

値がテストでのみ null になる場合、その値を非 null 許容としてマークし、テストで non-null 値を渡すようにすることで、コードを改善できます。

@required は新しい required キーワードとどう比較されますか?

#

@required アノテーションは、指定する必要がある名前付き引数をマークします。指定しない場合、アナライザーはヒントを報告します。

null safety では、非 null 許容型の名前付き引数は、デフォルト値を持つか、新しい required キーワードでマークされる必要があります。そうしないと、指定されなかった場合に null にデフォルト化されてしまうため、非 null 許容であることが意味をなさなくなります。

null safe コードがレガシーコードから呼び出される場合、required キーワードは @required アノテーションとまったく同じように扱われます。引数を指定しないと、アナライザーのヒントが発生します。

null safe コードが null safe コードから呼び出される場合、required 引数を指定しないとエラーになります。

移行においてこれは何を意味しますか? 以前は @required がなかった場所に required を追加する場合は注意してください。新しく必須となった引数を渡さない呼び出し元は、コンパイルできなくなります。代わりに、デフォルト値を追加するか、引数型を null 許容にすることができます。

final であるべきで、そうでない非 null 許容フィールドをどう移行すべきですか?

#

一部の計算は、静的イニシャライザに移動できます。以下のようにする代わりに、

baddart
// Initialized without values
ListQueue _context;
Float32List _buffer;
dynamic _readObject;

Vec2D(Map<String, dynamic> object) {
  _buffer = Float32List.fromList([0.0, 0.0]);
  _readObject = object['container'];
  _context = ListQueue<dynamic>();
}

次のようにできます。

gooddart
// Initialized with values
final ListQueue _context = ListQueue<dynamic>();
final Float32List _buffer = Float32List.fromList([0.0, 0.0]);
final dynamic _readObject;

Vec2D(Map<String, dynamic> object) : _readObject = object['container'];

ただし、コンストラクタで初期化されるフィールドは final にすることはできません。null safety では、これも non-nullable にすることが難しくなることがわかります。初期化が遅すぎると、初期化されるまで null であり、null 許容でなければなりません。幸い、選択肢があります。

  • コンストラクタをファクトリに変換し、すべてのフィールドを直接初期化する実際のコンストラクタに委譲するようにします。そのようなプライベートコンストラクタの一般的な名前は単なるアンダースコアです: _。すると、フィールドは final で non-nullable にすることができます。このリファクタリングは、null safety への移行に行うことができます。
  • または、フィールドを late final とマークします。これにより、一度だけ初期化されることが保証されます。読み取る前に初期化する必要があります。

built_value クラスをどう移行すべきですか?

#

@nullable とアノテートされていたゲッターは、代わりに null 許容型を持ち、その後すべての @nullable アノテーションを削除してください。たとえば、

dart
@nullable
int get count;

は次のようになります。

dart
int? get count; //  Variable initialized with ?

@nullable とマークされていなかったゲッターは、移行ツールがそれらを提案したとしても、null 許容型を持つべきではありません。必要に応じて ! ヒントを追加してから、分析を再実行してください。

null を返す可能性のあるファクトリをどう移行すべきですか?

#

null を返さないファクトリを優先してください。 無効な入力のために例外をスローすることを意図していたコードが、代わりに null を返してしまうケースが見られました。

以下のようにする代わりに、

baddart
  factory StreamReader(dynamic data) {
    StreamReader reader;
    if (data is ByteData) {
      reader = BlockReader(data);
    } else if (data is Map) {
      reader = JSONBlockReader(data);
    }
    return reader;
  }

次のようにします。

gooddart
  factory StreamReader(dynamic data) {
    if (data is ByteData) {
      // Move the readIndex forward for the binary reader.
      return BlockReader(data);
    } else if (data is Map) {
      return JSONBlockReader(data);
    } else {
      throw ArgumentError('Unexpected type for data');
    }
  }

ファクトリの意図が実際に null を返すことであった場合、それを static メソッドに変換して null を返すことができるようにすることができます。

不要になった assert(x != null) をどう移行すべきですか?

#

すべてが完全に移行されるとアサートは不要になりますが、現時点ではチェックを維持したいのであれば必要です。選択肢は次のとおりです。

  • アサートは実際には不要であると判断し、削除します。これは、アサートが有効になっている場合の動作の変更です。
  • アサートは常にチェックできると判断し、ArgumentError.checkNotNull に変換します。これは、アサートが無効になっている場合の動作の変更です。
  • 動作を正確に維持する: 警告をバイパスするために // ignore: unnecessary_null_comparison を追加します。

不要になった実行時のnullチェックをどう移行すべきですか?

#

arg を非 null 許容にすると、コンパイラは明示的な実行時 null チェックを不要な比較としてフラグ付けします。

dart
if (arg == null) throw ArgumentError(...)`

プログラムが混在バージョンである場合は、このチェックを含める必要があります。すべてが完全に移行され、コードがサウンド null safety で実行されるようになるまで、argnull に設定される可能性があります。

動作を維持する最も簡単な方法は、チェックを ArgumentError.checkNotNull に変更することです。

一部の実行時型チェックにも同様のことが当てはまります。arg の静的型が String の場合、if (arg is! String) は実際には argnull かどうかをチェックしています。null safety への移行により arg が決して null になることはないようになると思えるかもしれませんが、サウンドではない null safety では null になる可能性があります。したがって、動作を維持するために、null チェックはそのままにしておく必要があります。

Iterable.firstWhere メソッドは orElse: () => null を受け入れなくなりました。

#

package:collection をインポートし、firstWhere の代わりに拡張メソッド firstWhereOrNull を使用してください。

セッターを持つ属性をどう扱えばよいですか?

#

上記の late final の提案とは異なり、これらの属性は final とマークすることはできません。多くの場合、セッター可能な属性には初期値がなく、後で設定されることが期待されるためです。

そのような場合、2つの選択肢があります。

  • 初期値を設定します。多くの場合、初期値の省略は意図的ではなく、間違いです。

  • 属性がアクセスされる前に必ず設定する必要があると確信している場合は、late とマークしてください。

    警告: late キーワードは実行時チェックを追加します。ユーザーが set する前に get を呼び出すと、実行時にエラーが発生します。

Mapの戻り値が非null許容であることをどう示せますか?

#

Map のルックアップ演算子 ([]) は、デフォルトでは null 許容型を返します。値が確実に存在することを示す方法はありません。

この場合、非 null アサーション演算子 (!) を使用して値を V にキャストする必要があります。

dart
return blockTypes[key]!;

これは、マップが null を返した場合に例外をスローします。そのケースを明示的に処理したい場合は、

dart
var result = blockTypes[key];
if (result != null) return result;
// Handle the null case here, e.g. throw with explanation.

List/Mapのジェネリック型がnull許容なのはなぜですか?

#

通常、このような null 許容コードになるのはコードの悪臭です。

baddart
List<Foo?> fooList; // fooList can contain null values

これは fooList に null 値が含まれる可能性があることを意味します。これは、リストを長さで初期化し、ループで埋める場合に発生する可能性があります。

単純に同じ値でリストを初期化している場合は、代わりに filled コンストラクタを使用してください。

baddart
_jellyCounts = List<int?>(jellyMax + 1);
for (var i = 0; i <= jellyMax; i++) {
  _jellyCounts[i] = 0; // List initialized with the same value
}
gooddart
_jellyCounts = List<int>.filled(jellyMax + 1, 0); // List initialized with filled constructor

インデックスで要素を設定している場合、またはリストの各要素を個別の値で populat している場合は、代わりにリストリテラル構文を使用してリストを構築してください。

baddart
_jellyPoints = List<Vec2D?>(jellyMax + 1);
for (var i = 0; i <= jellyMax; i++) {
  _jellyPoints[i] = Vec2D(); // Each list element is a distinct Vec2D
}
gooddart
_jellyPoints = [
  for (var i = 0; i <= jellyMax; i++)
    Vec2D() // Each list element is a distinct Vec2D
];

固定長リストを生成するには、growable パラメータを false に設定した List.generate コンストラクタを使用します。

dart
_jellyPoints = List.generate(jellyMax, (_) => Vec2D(), growable: false);

デフォルトのListコンストラクタはどうなりましたか?

#

このエラーに遭遇する可能性があります。

The default 'List' constructor isn't available when null safety is enabled. #default_list_constructor

デフォルトのリストコンストラクタはリストを null で埋めます。これは問題です。

代わりに List.filled(length, default) に変更してください。

package:ffi を使用しており、移行時に Dart_CObject_kUnsupported というエラーが発生します。どうなりましたか?

#

ffi 経由で送信されるリストは List<dynamic> のみで、List<Object>List<Object?> にすることはできません。移行でリスト型を明示的に変更しなかった場合、null safety を有効にしたときの型推論の変更により、型が変更されている可能性があります。

修正方法は、このようなリストを明示的に List<dynamic> として作成することです。

移行ツールはなぜコードにコメントを追加するのですか?

#

移行ツールは、サウンドモードで実行中に常に false または true になる条件を検出すると、/* == false */ または /* == true */ コメントを追加します。このようなコメントは、自動移行が間違っており、人間の介入が必要であることを示している可能性があります。たとえば、

dart
if (registry.viewFactory(viewDescriptor.id) == null /* == false */)

このような場合、移行ツールは防御的コーディングの状況と、null 値が実際に期待される状況を区別できません。そのため、ツールはわかっていること(「この条件は常に false になるようです!」)を伝え、ユーザーに判断を委ねます。

JavaScript へのコンパイルと null safety について、何をทราบしておくべきですか?

#

Null safety は、コードサイズの削減やアプリパフォーマンスの向上など、多くのメリットをもたらします。これらのメリットは、Flutter や AOT などのネイティブターゲットにコンパイルした場合に、より顕著に現れます。以前のプロダクション Web コンパイラでの作業は、null safety が後に導入したものと同様の最適化を導入していました。これにより、プロダクション Web アプリでの具体的な改善が、ネイティブターゲットよりも少なく見える可能性があります。

強調する価値のある点がいくつかあります。

  • プロダクション JavaScript コンパイラは ! 非 null アサーションを生成します。非 null アサーションを追加する前後のコンパイラ出力の比較では、それに気づかないかもしれません。これは、コンパイラが null safety がないプログラムでもすでに null チェックを生成していたためです。

  • コンパイラは、null safety のサウンドネスや最適化レベルに関係なく、これらの非 null アサーションを生成します。実際、コンパイラは -O3 または --omit-implicit-checks を使用しても ! を削除しません。

  • プロダクション JavaScript コンパイラは、不要な null チェックを削除する場合があります。これは、null safety より前にプロダクション Web コンパイラが行った最適化により、値が null でないことがわかっていた場合にこれらのチェックが削除されたためです。

  • デフォルトでは、コンパイラはパラメータサブタイプチェックを生成します。これらの実行時チェックは、共変仮想呼び出しに適切な引数があることを保証します。コンパイラは --omit-implicit-checks オプションでこれらのチェックをスキップします。このオプションを使用すると、コードに無効な型が含まれている場合、予期しない動作をするアプリが生成される可能性があります。驚きを避けるために、コードの強力なテストカバレッジを提供し続けてください。特に、コンパイラは入力が型宣言に準拠するという事実にに基づいてコードを最適化します。コードが無効な型の引数を提供した場合、それらの最適化は誤りとなり、プログラムは誤動作する可能性があります。これは以前の一貫性のない型でも同様であり、サウンド null safety を備えた現在の一貫性のない null許容性でも同様です。

  • 開発 JavaScript コンパイラと Dart VM には null チェックの特別なエラーメッセージがありますが、アプリケーションを小さく保つために、プロダクション JavaScript コンパイラにはありません。

  • null 上で .toString が見つからないことを示すエラーが表示される場合があります。これはバグではありません。コンパイラは常にこのように一部の null チェックをエンコードしてきました。つまり、コンパイラはレシーバーのプロパティにガードされていないアクセスを行うことで、一部の null チェックをコンパクトに表現します。たとえば、if (a == null) throw の代わりに、a.toString を生成します。toString メソッドは JavaScript Object に定義されており、オブジェクトが null でないことを検証する高速な方法です。

    null チェックの直後に null でクラッシュするアクションが最初の操作である場合、コンパイラは null チェックを削除し、アクションがエラーを引き起こすようにすることができます。

    たとえば、Dart 式 print(a!.foo()); は直接

    js
      P.print(a.foo$0());

    これは、a が null の場合に a.foo$() の呼び出しがクラッシュするためです。コンパイラが foo をインライン化する場合、null チェックは保持されます。たとえば、fooint foo() => 1; であった場合、コンパイラは

    js
      a.toString;
      P.print(1);

    インライン化されたメソッドがレシーバーのフィールドに最初にアクセスした場合(int foo() => this.x + 1; のような)、プロダクションコンパイラは冗長な a.toString null チェックを、インライン化されない呼び出しと同様に削除し、

    js
      P.print(a.x + 1);

リソース

#