目次

Nullセーフティ:よくある質問

目次 keyboard_arrow_down keyboard_arrow_up
more_horiz

このページでは、Google社内コードの移行経験に基づいて、Nullセーフティについてよく寄せられる質問をまとめています。

移行されたコードのユーザーにとって、どのようなランタイムの変更に注意する必要がありますか?

#

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

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

注意すべき2つの例外は

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

値がテストでのみnullの場合はどうですか?

#

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

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

#

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

Nullセーフティでは、非Null許容型の名前付き引数には、デフォルト値を設定するか、新しい`required`キーワードでマークする必要があります。そうでない場合、渡されないときにデフォルトでnullになるため、非Null許容にするのは意味がありません。

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

NullセーフコードがNullセーフコードから呼び出された場合、`required`引数を指定しないとエラーになります。

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

finalにする必要があるが、そうでない非Null許容フィールドをどのように移行する必要がありますか?

#

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

悪いdart
// 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>();
}

あなたはすることができます

良いdart
// 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セーフティでは、これが非Null許容にすることも難しくなることがわかります。初期化が遅すぎると、初期化されるまでnullになり、Null許容にする必要があります。幸いなことに、選択肢があります

  • コンストラクタをファクトリに変換し、すべてのフィールドを直接初期化する実際のコンストラクタに委任します。このようなプライベートコンストラクタの一般的な名前は、単なるアンダースコアです:`_`。その後、フィールドは`final`および非Null許容にすることができます。このリファクタリングは、Nullセーフティへの移行の*前*に行うことができます。
  • または、フィールドを`late final`とマークします。これは、それが正確に1回初期化されることを強制します。読み取る前に初期化する必要があります。

built_valueクラスをどのように移行する必要がありますか?

#

@nullableとアノテーションが付けられたゲッターは、代わりにNull許容型を持つ必要があります。次に、すべての@nullableアノテーションを削除します。例えば

dart
@nullable
int get count;

なります

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

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

nullを返すことができるファクトリをどのように移行する必要がありますか?

#

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

代わりに

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

行う

良いdart
  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を返すことであった場合、静的メソッドに変換して、nullを返すことができるようにすることができます。

不要と表示されるようになったassert(x != null)をどのように移行する必要がありますか?

#

すべてが完全に移行されるとassertは不要になりますが、今のところチェックを維持したい場合は*必要*です。オプション

  • assertは実際には必要ないと判断し、削除します。これは、assertが有効になっている場合の動作の変更です。
  • assertは常にチェックできると判断し、`ArgumentError.checkNotNull`に変えます。これは、assertが無効になっている場合の動作の変更です。
  • 動作をそのまま維持します:警告を回避するために、`// ignore: unnecessary_null_comparison`を追加します。

不要と表示されるようになったランタイムnullチェックをどのように移行する必要がありますか?

#

コンパイラは、`arg`を非Null許容にすると、明示的なランタイムNullチェックに不要な比較としてフラグを立てます。

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

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

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

同じことが一部のランタイム型チェックにも当てはまります。 `arg`の静的型が`String`の場合、`if (arg is! String)`は実際には`arg`が`null`かどうかをチェックしています。Nullセーフティに移行すると`arg`が`null`になることはないと考えられるかもしれませんが、健全でないNullセーフティでは`null`になる可能性があります。そのため、動作を維持するには、Nullチェックをそのままにする必要があります。

Iterable.firstWhereメソッドは、もはや`orElse: () => null`を受け入れません。

#

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

セッターを持つ属性をどのように処理すればよいですか?

#

上記の `late final` の提案とは異なり、これらの属性は final としてマークすることはできません。多くの場合、設定可能な属性は後で設定されることが想定されているため、初期値を持ちません。

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

  • 初期値を設定する。多くの場合、初期値の省略は意図的なものではなく、誤りです。

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

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

Mapからの戻り値が非Null許容であることをどのように示せばよいですか?

#

Map のルックアップ演算子(`[]`)は、デフォルトで 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 許容コードになるのは、一般的にコードの臭いです。

悪いdart
List<Foo?> fooList; // fooList can contain null values

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

リストを同じ値で初期化するだけの場合は、代わりに`filled` コンストラクタを使用する必要があります。

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

インデックスを介してリストの要素を設定する場合、またはリストの各要素に異なる値を設定する場合は、代わりにリストリテラル構文を使用してリストを作成する必要があります。

悪いdart
_jellyPoints = List<Vec2D?>(jellyMax + 1);
for (var i = 0; i <= jellyMax; i++) {
  _jellyPoints[i] = Vec2D(); // Each list element is a distinct Vec2D
}
良いdart
_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<Object>` または `List<Object?>` ではなく、`List<dynamic>` である必要があります。移行でリスト型を明示的に変更しなかった場合でも、null セーフティを有効にしたときに発生する型推論の変更により、型が変更されている可能性があります。

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

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

#

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

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

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

JavaScriptへのコンパイルとNullセーフティについて知っておくべきことは何ですか?

#

Null セーフティは、コードサイズの削減やアプリのパフォーマンスの向上など、多くの利点をもたらします。このような利点は、Flutter や AOT などのネイティブターゲットにコンパイルするときに、より顕著になります。本番用Webコンパイラに関する以前の作業では、nullセーフティが後で導入したものと同様の最適化が導入されていました。これにより、本番用Webアプリの結果的なゲインがネイティブターゲットよりも少なくなる可能性があります。

強調する価値のあるいくつかの注意事項

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

  • コンパイラは、null セーフティの健全性や最適化レベルに関係なく、これらの null アサーションを生成します。実際、コンパイラは `-O3` または `--omit-implicit-checks` を使用しても `!` を削除しません。

  • 本番用 JavaScript コンパイラは、不要な null チェックを削除する場合があります。これは、null セーフティより前に本番用Webコンパイラが行った最適化によって、値が null でないことがわかっている場合にそれらのチェックが削除されたためです。

  • デフォルトでは、コンパイラはパラメータのサブタイプチェックを生成します。これらの実行時チェックにより、共変な仮想呼び出しに適切な引数が渡されます。コンパイラは、`--omit-implicit-checks` オプションを使用すると、これらのチェックをスキップします。このオプションを使用すると、コードに無効な型が含まれている場合、アプリが予期しない動作をする可能性があります。予期しない事態を避けるため、コードの強力なテストカバレッジを引き続き提供してください。特に、コンパイラは、入力が型宣言に準拠している必要があるという事実 استنادしてコードを最適化します。コードが無効な型の引数を指定した場合、それらの最適化は間違っており、プログラムが誤動作する可能性があります。これは以前は不整合な型の場合に当てはまりましたが、健全な null セーフティにより、不整合な 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 チェックは保持されます。たとえば、`foo` が `int foo() => 1;` であった場合、コンパイラは次のように生成する可能性があります。

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

    インライン化されたメソッドが最初にレシーバのフィールドにアクセスした場合(例:`int foo() => this.x + 1;`)、本番用コンパイラは冗長な `a.toString` null チェックをインライン化されていない呼び出しとして削除し、次のように生成できます。

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

リソース

#