目次

型プロモーションの失敗の修正

型プロモーション は、フロー解析によって、nullable 型 の変数が null でないこと、そしてその時点以降変更されないことが確実に確認できる場合に発生します。多くの状況で型の堅牢性が弱まり、型プロモーションが失敗する原因となります。

このページでは、型プロモーションの失敗が発生する理由を、修正方法のヒントとともにリストしています。フロー解析と型プロモーションの詳細については、Null セーフティの理解 ページをご覧ください。

フィールドプロモーションでサポートされていない言語バージョン

#

原因: フィールドのプロモーションを試行していますが、フィールドプロモーションは言語バージョンに依存しており、コードは3.2より前の言語バージョンに設定されています。

SDK バージョン >= Dart 3.2 を既に使用している場合でも、コードは以前の言語バージョンを明示的に対象としている可能性があります。これは、以下のいずれかの理由で発生する可能性があります。

  • pubspec.yaml で、3.2 より低い下限を持つ SDK 制約を宣言している場合、または
  • ファイルの先頭に // @dart=version コメントがあり、version が 3.2 より低い場合。

悪い例dart
// @dart=3.1

class C {
  final int? _i;
  C(this._i);

  void f() {
    if (_i != null) {
      int i = _i;  // ERROR
    }
  }
}

メッセージ

'_i' refers to a field. It couldn't be promoted because field promotion is only available in Dart 3.2 and above.

解決策

ライブラリが 3.2 より前の言語バージョンを使用していないことを確認します。ファイルの先頭にある古い // @dart=version コメント、または 古い SDK 制約の下限 を持つ pubspec.yaml を確認してください。

(Dart 3.2 より前では) ローカル変数のみをプロモーションできます

#

原因: プロパティのプロモーションを試行していますが、3.2 より前の Dart バージョンではローカル変数のみをプロモーションでき、3.2 より前のバージョンを使用しています。

悪い例dart
class C {
  int? i;
  void f() {
    if (i == null) return;
    print(i.isEven);       // ERROR
  }
}

メッセージ

'i' refers to a property so it couldn't be promoted.

解決策

Dart 3.1 以前を使用している場合は、3.2 以降にアップグレードしてください

古いバージョンを引き続き使用する必要がある場合は、その他の原因と回避策 を参照してください。

その他の原因と回避策

#

このページの残りの例では、フィールドとローカル変数の両方のプロモーションの失敗の理由を、バージョンの一貫性の問題とは無関係に、例と回避策とともに説明しています。

一般的に、プロモーションの失敗に対する通常の修正は、以下のいずれか、または複数です。

  • 必要な非 nullable 型を持つローカル変数にプロパティの値を代入します。
  • 明示的な null チェックを追加します (例: i == null)。
  • 式が null ではないと確信している場合は、冗長なチェックとして ! または as を使用します。

i の値を保持するローカル変数 (i という名前を付けることができます) を作成する例を以下に示します。

良い例dart
class C {
  int? i;
  void f() {
    final i = this.i;
    if (i == null) return;
    print(i.isEven);
  }
}

この例はインスタンスフィールドを示していますが、代わりにインスタンスゲッター、静的フィールドまたはゲッター、トップレベル変数またはゲッター、またはthis を使用できます。

そして、i! を使用した例を以下に示します。

良い例dart
print(i!.isEven);

this をプロモーションできない

#

原因: this をプロモーションしようとしていますが、this の型プロモーションはまだサポートされていません。

拡張メソッド を記述する場合、一般的な this プロモーションのシナリオの1つは、拡張メソッドのon が nullable 型である場合に、thisnull であるかどうかを確認する null チェックを行うことです。

悪い例dart
extension on int? {
  int get valueOrZero {
    return this == null ? 0 : this; // ERROR
  }
}

メッセージ

`this` can't be promoted.

解決策

this の値を保持するローカル変数を作成し、null チェックを実行します。

良い例dart
extension on int? {
  int get valueOrZero {
    final self = this;
    return self == null ? 0 : self;
  }
}

プライベートフィールドのみをプロモーションできます

#

原因: フィールドのプロモーションを試行していますが、そのフィールドはプライベートではありません。

プログラムの他のライブラリが、ゲッターを使用してパブリックフィールドをオーバーライドすることは可能です。ゲッターは安定した値を返さない可能性があるため、コンパイラは他のライブラリが何をしているかを知ることができないため、非プライベートフィールドはプロモーションできません。

悪い例dart
class Example {
  final int? value;
  Example(this.value);
}

void test(Example x) {
  if (x.value != null) {
    print(x.value + 1); // ERROR
  }
}

メッセージ

'value' refers to a public property so it couldn't be promoted.

解決策

フィールドをプライベートにすることで、外部ライブラリがその値をオーバーライドする可能性がないことをコンパイラが確実に確認できるため、プロモーションが安全になります。

良い例dart
class Example {
  final int? _value;
  Example(this._value);
}

void test(Example x) {
  if (x._value != null) {
    print(x._value + 1);
  }
}

final フィールドのみをプロモーションできます

#

原因: フィールドのプロモーションを試行していますが、そのフィールドは final ではありません。

コンパイラにとって、非 final フィールドは原則として、テストされた時点と使用された時点の間のいつでも変更される可能性があります。そのため、コンパイラが非 final の nullable 型を非 nullable 型にプロモーションすることは安全ではありません。

悪い例dart
class Example {
  int? _mutablePrivateField;
  Example(this._mutablePrivateField);

  void f() {
    if (_mutablePrivateField != null) {
      int i = _mutablePrivateField; // ERROR
    }
  }
}

メッセージ

'_mutablePrivateField' refers to a non-final field so it couldn't be promoted.

解決策

フィールドを final にします。

良い例dart
class Example {
  final int? _immutablePrivateField;
  Example(this._immutablePrivateField);

  void f() {
    if (_immutablePrivateField != null) {
      int i = _immutablePrivateField; // OK
    }
  }
}

ゲッターはプロモーションできません

#

原因: ゲッターのプロモーションを試行していますが、プロモーションできるのはインスタンスフィールドだけであり、インスタンスゲッターではありません。

コンパイラは、ゲッターが毎回同じ結果を返すことを保証する方法がありません。安定性が確認できないため、ゲッターはプロモーションできません。

悪い例dart
import 'dart:math';

abstract class Example {
  int? get _value => Random().nextBool() ? 123 : null;
}

void f(Example x) {
  if (x._value != null) {
    print(x._value.isEven); // ERROR
  }
}

メッセージ

'_value' refers to a getter so it couldn't be promoted.

解決策

ゲッターをローカル変数に代入します。

良い例dart
import 'dart:math';

abstract class Example {
  int? get _value => Random().nextBool() ? 123 : null;
}

void f(Example x) {
  final value = x._value;
  if (value != null) {
    print(value.isEven); // OK
  }
}

外部フィールドはプロモーションできません

#

原因: フィールドのプロモーションを試行していますが、そのフィールドは external とマークされています。

外部フィールドは、基本的に外部ゲッターであるためプロモーションされません。その実装は Dart の外部のコードであるため、外部フィールドが呼び出されるたびに同じ値を返すという保証はコンパイラにはありません。

悪い例dart
class Example {
  external final int? _externalField;

  void f() {
    if (_externalField != null) {
      print(_externalField.isEven); // ERROR
    }
  }
}

メッセージ

'_externalField' refers to an external field so it couldn't be promoted.

解決策

外部フィールドの値をローカル変数に代入します。

良い例dart
class Example {
  external final int? _externalField;

  void f() {
    final i = _externalField;
    if (i != null) {
      print(i.isEven); // OK
    }
  }
}

ライブラリの他の場所でゲッターと競合しています

#

原因: フィールドのプロモーションを試行していますが、同じライブラリの別のクラスに、同じ名前の具体的なゲッターが含まれています。

悪い例dart
import 'dart:math';

class Example {
  final int? _overridden;
  Example(this._overridden);
}

class Override implements Example {
  @override
  int? get _overridden => Random().nextBool() ? 1 : null;
}

void testParity(Example x) {
  if (x._overridden != null) {
    print(x._overridden.isEven); // ERROR
  }
}

メッセージ

'_overriden' couldn't be promoted because there is a conflicting getter in class 'Override'.

解決策:

ゲッターとフィールドが関連していて、名前を共有する必要がある場合 (上の例のように、一方のゲッターがもう一方をオーバーライドする場合など)、ローカル変数に値を代入することで型プロモーションを有効にできます。

良い例dart
import 'dart:math';

class Example {
  final int? _overridden;
  Example(this._overridden);
}

class Override implements Example {
  @override
  int? get _overridden => Random().nextBool() ? 1 : null;
}

void testParity(Example x) {
  final i = x._overridden;
  if (i != null) {
    print(i.isEven); // OK
  }
}

関連のないクラスに関する注記

#

上記の例では、フィールド _overridden をプロモーションすることが安全ではない理由が明確です。フィールドとゲッターの間にオーバーライドの関係があるためです。ただし、クラスが関連していない場合でも、競合するゲッターがあるとフィールドのプロモーションは妨げられます。例:

悪い例dart
import 'dart:math';

class Example {
  final int? _i;
  Example(this._i);
}

class Unrelated {
  int? get _i => Random().nextBool() ? 1 : null;
}

void f(Example x) {
  if (x._i != null) {
    int i = x._i; // ERROR
  }
}

別のライブラリには、関連のない2つのクラスを同じクラス階層にまとめるクラスが含まれている場合があります。これにより、関数fにおけるx._iへの参照がUnrelated._iにディスパッチされることになります。例:

悪い例dart
class Surprise extends Unrelated implements Example {}

void main() {
  f(Surprise());
}

解決策

フィールドと競合するエンティティが本当に関連していない場合は、異なる名前を付けることで問題を回避できます。

良い例dart
class Example {
  final int? _i;
  Example(this._i);
}

class Unrelated {
  int? get _j => Random().nextBool() ? 1 : null;
}

void f(Example x) {
  if (x._i != null) {
    int i = x._i; // OK
  }
}

ライブラリの他の場所でプロモーションできないフィールドと競合しています

#

原因: フィールドのプロモーションを試みていますが、同じライブラリの別のクラスに、プロモーションできない(このページに記載されている他の理由のいずれかによる)同じ名前のフィールドが含まれています。

悪い例dart
class Example {
  final int? _overridden;
  Example(this._overridden);
}

class Override implements Example {
  @override
  int? _overridden;
}

void f(Example x) {
  if (x._overridden != null) {
    print(x._overridden.isEven); // ERROR
  }
}

この例は、実行時にxが実際にはOverrideのインスタンスである可能性があるため、プロモーションが安全ではないため失敗します。

メッセージ

'overridden' couldn't be promoted because there is a conflicting non-promotable field in class 'Override'.

解決策

フィールドが実際に関連していて、名前を共有する必要がある場合は、値をfinalなローカル変数に代入してプロモーションすることで、型プロモーションを有効にできます。

良い例dart
class Example {
  final int? _overridden;
  Example(this._overridden);
}

class Override implements Example {
  @override
  int? _overridden;
}

void f(Example x) {
  final i = x._overridden;
  if (i != null) {
    print(i.isEven); // OK
  }
}

フィールドが関連していない場合は、フィールドのいずれかの名前を変更して、競合しないようにします。関連のないクラスに関する注記を参照してください。

暗黙的なnoSuchMethodフォワーダーとの競合

#

原因: プライベートでfinalなフィールドのプロモーションを試みていますが、同じライブラリの別のクラスに、フィールドと同じ名前の暗黙的なnoSuchMethodフォワーダーが含まれています。

これは、noSuchMethodが呼び出しごとに安定した値を返すという保証がないため、安全ではありません。

悪い例dart
import 'package:mockito/mockito.dart';

class Example {
  final int? _i;
  Example(this._i);
}

class MockExample extends Mock implements Example {}

void f(Example x) {
  if (x._i != null) {
    int i = x._i; // ERROR
  }
}

この例では、_iは、コンパイラがMockExample内で生成する(_iという名前の)安全ではない暗黙的なnoSuchMethodフォワーダーに解決される可能性があるため、プロモーションできません。

コンパイラは、MockExampleが宣言でExampleを実装する際に_iのゲッターをサポートすることを約束しているが、その約束を果たしていないため、_iのこの暗黙的な実装を作成します。そのため、未定義のゲッター実装はMocknoSuchMethod定義によって処理され、同じ名前の暗黙的なnoSuchMethodフォワーダーが作成されます。

関連のないクラスのフィールド間でも、このエラーが発生する可能性があります。

メッセージ

'_i' couldn't be promoted because there is a conflicting noSuchMethod forwarder in class 'MockExample'.

解決策

noSuchMethodが暗黙的に実装を処理する必要がないように、問題のゲッターを定義します。

良い例dart
import 'package:mockito/mockito.dart';

class Example {
  final int? _i;
  Example(this._i);
}

class MockExample extends Mock implements Example {
  @override
  late final int? _i;
}

void f(Example x) {
  if (x._i != null) {
    int i = x._i; // OK
  }
}

ゲッターは、モックの一般的な使用方法と一致するようにlateと宣言されています。モックを含まないシナリオでは、この型プロモーションエラーを解決するために、ゲッターをlateと宣言する必要はありません。

プロモーション後に書き込まれている可能性があります

#

原因: プロモーションされてから書き換えられた可能性のある変数のプロモーションを試みています。

悪い例dart
void f(bool b, int? i, int? j) {
  if (i == null) return;
  if (b) {
    i = j;           // (1)
  }
  if (!b) {
    print(i.isEven); // (2) ERROR
  }
}

解決策:

この例では、フロー解析が(1)に到達すると、iはnull許容でないintからnull許容のint?にデモートされます。(2)へのアクセスは、(1)と(2)の両方を含むコードパスがないため安全であると人間は判断できますが、フロー解析は別のif文の条件間の相関関係を追跡しないため、それを認識できません。

2つのif文を組み合わせることで問題を修正できる場合があります。

良い例dart
void f(bool b, int? i, int? j) {
  if (i == null) return;
  if (b) {
    i = j;
  } else {
    print(i.isEven);
  }
}

このような直線的な制御フローの場合(ループなし)、フロー解析は、デモートするかどうかを決定する際に代入の右辺を考慮します。その結果、このコードを修正するもう1つの方法は、jの型をintに変更することです。

良い例dart
void f(bool b, int? i, int j) {
  if (i == null) return;
  if (b) {
    i = j;
  }
  if (!b) {
    print(i.isEven);
  }
}

前のループ反復で書き込まれている可能性があります

#

原因: ループの前の繰り返しで書き換えられた可能性のあるものをプロモーションしようとしているため、プロモーションが無効になりました。

悪い例dart
void f(Link? p) {
  if (p != null) return;
  while (true) {    // (1)
    print(p.value); // (2) ERROR
    var next = p.next;
    if (next == null) break;
    p = next;       // (3)
  }
}

フロー解析が(1)に到達すると、先を見据えて(3)のpへの書き込みを確認します。しかし、先を見据えているため、代入の右辺の型はまだ判明しておらず、プロモーションを保持できるかどうかが不明です。安全のために、プロモーションは無効になります。

解決策:

この問題を修正するには、nullチェックをループの先頭に移動します。

良い例dart
void f(Link? p) {
  while (p != null) {
    print(p.value);
    p = p.next;
  }
}

ラベル付きswitch文を使用してループを作成できるため、caseブロックにラベルがある場合、switch文でも同様の状況が発生する可能性があります。

悪い例dart
void f(int i, int? j, int? k) {
  if (j == null) return;
  switch (i) {
    label:
    case 0:
      print(j.isEven); // ERROR
      j = k;
      continue label;
  }
}

繰り返しますが、この問題を修正するには、nullチェックをループの先頭に移動します。

良い例dart
void f(int i, int? j, int? k) {
  switch (i) {
    label:
    case 0:
      if (j == null) return;
      print(j.isEven);
      j = k;
      continue label;
  }
}

try ブロックで書き込みが行われた可能性のある catch ブロック内

#

原因: 変数がtryブロックで書き換えられ、現在catchブロックで実行されています。

悪い例dart
void f(int? i, int? j) {
  if (i == null) return;
  try {
    i = j;                 // (1)
    // ... Additional code ...
    if (i == null) return; // (2)
    // ... Additional code ...
  } catch (e) {
    print(i.isEven);       // (3) ERROR
  }
}

この場合、フロー解析はi.isEven (3)を安全とは見なしません。tryブロックのどこで例外が発生したかを知る方法がないため、保守的に、iがnullの可能性があった(1)と(2)の間で発生した可能性があると想定しています。

tryfinallyブロックの間、およびcatchfinallyブロックの間でも同様の状況が発生する可能性があります。実装方法の履歴的アーティファクトのため、これらのtry/catch/finallyの状況では、ループの場合と同様に、代入の右辺は考慮されません。

解決策:

問題を修正するには、catchブロックが、tryブロック内で変更される変数の状態に関する仮定に依存しないようにします。例外は、tryブロックのいつでも、iがnullの可能性があるときに発生する可能性があることを忘れないでください。

最も安全な解決策は、catchブロック内にnullチェックを追加することです。

良い例dart
try {
  // ···
} catch (e) {
  if (i != null) {
    print(i.isEven); // (3) OK due to the null check in the line above.
  } else {
    // Handle the case where i is null.
  }
}

または、iがnullである間に例外が発生する可能性がないと確信している場合は、!演算子を使用してください。

dart
try {
  // ···
} catch (e) {
  print(i!.isEven); // (3) OK because of the `!`.
}

サブタイプミスマッチ

#

原因: プロモーションしようとしている型が、変数の現在のプロモーション済み型のサブタイプではない(またはプロモーション試行時のサブタイプではなかった)場合。

悪い例dart
void f(Object o) {
  if (o is Comparable /* (1) */) {
    if (o is Pattern /* (2) */) {
      print(o.matchAsPrefix('foo')); // (3) ERROR
    }
  }
}

この例では、oは(1)でComparableにプロモーションされますが、(2)でPatternにプロモーションされません。これは、PatternComparableのサブタイプではないためです。(その理由は、プロモーションされた場合、Comparableのメソッドを使用できなくなるためです。)PatternComparableのサブタイプではないからといって、(3)のコードがデッドコードであるという意味ではありません。oは、ComparablePatternの両方を実装する(Stringなど)型を持つ可能性があります。

解決策:

1つの可能な解決策は、新しいローカル変数を作成して、元の変数をComparableにプロモーションし、新しい変数をPatternにプロモーションすることです。

dart
void f(Object o) {
  if (o is Comparable /* (1) */) {
    Object o2 = o;
    if (o2 is Pattern /* (2) */) {
      print(
          o2.matchAsPrefix('foo')); // (3) OK; o2 was promoted to `Pattern`.
    }
  }
}

しかし、後でコードを編集する人が、Object o2var o2に変更する可能性があります。この変更により、o2の型はComparableになり、オブジェクトをPatternにプロモーションできないという問題が再び発生します。

冗長な型チェックの方が良い解決策かもしれません。

良い例dart
void f(Object o) {
  if (o is Comparable /* (1) */) {
    if (o is Pattern /* (2) */) {
      print((o as Pattern).matchAsPrefix('foo')); // (3) OK
    }
  }
}

もう1つの解決策は、より正確な型を使用できる場合に機能します。行3が文字列のみに関心がある場合は、型チェックでStringを使用できます。StringComparableのサブタイプであるため、プロモーションは機能します。

良い例dart
void f(Object o) {
  if (o is Comparable /* (1) */) {
    if (o is String /* (2) */) {
      print(o.matchAsPrefix('foo')); // (3) OK
    }
  }
}

ローカル関数によってキャプチャされた書き込み

#

原因: 変数がローカル関数または関数式によって書き込みキャプチャされています。

悪い例dart
void f(int? i, int? j) {
  var foo = () {
    i = j;
  };
  // ... Use foo ... 
  if (i == null) return; // (1)
  // ... Additional code ...
  print(i.isEven);       // (2) ERROR
}

フロー解析は、fooの定義に到達するとすぐに、いつでも呼び出される可能性があるため、iを完全にプロモーションすることはもはや安全ではないと判断します。ループと同様に、このデモートは代入の右辺の型に関係なく発生します。

解決策:

場合によっては、ロジックを再構成して、書き込みキャプチャの前にプロモーションを行うことができます。

良い例dart
void f(int? i, int? j) {
  if (i == null) return; // (1)
  // ... Additional code ...
  print(i.isEven); // (2) OK
  var foo = () {
    i = j;
  };
  // ... Use foo ...
}

別のオプションは、ローカル変数を作成して、書き込みキャプチャされないようにすることです。

良い例dart
void f(int? i, int? j) {
  var foo = () {
    i = j;
  };
  // ... Use foo ...
  var i2 = i;
  if (i2 == null) return; // (1)
  // ... Additional code ...
  print(i2.isEven); // (2) OK because `i2` isn't write captured.
}

または、冗長なチェックを行うことができます。

dart
void f(int? i, int? j) {
  var foo = () {
    i = j;
  };
  // ... Use foo ...
  if (i == null) return; // (1)
  // ... Additional code ...
  print(i!.isEven); // (2) OK due to `!` check.
}

現在のクロージャまたは関数式の外で書き込まれています

#

原因: 変数はクロージャまたは関数式の外部で書き換えられており、型プロモーションの位置はクロージャまたは関数式の内部です。

悪い例dart
void f(int? i, int? j) {
  if (i == null) return;
  var foo = () {
    print(i.isEven); // (1) ERROR
  };
  i = j;             // (2)
}

フロー解析は、fooがいつ呼び出されるかを判断する方法がないため、(2)の代入後に呼び出される可能性があり、そのためプロモーションが無効になる可能性があると判断します。ループと同様に、このデモートは代入の右辺の型に関係なく発生します。

解決策:

解決策は、ローカル変数を作成することです。

良い例dart
void f(int? i, int? j) {
  if (i == null) return;
  var i2 = i;
  var foo = () {
    print(i2.isEven); // (1) OK because `i2` isn't changed later.
  };
  i = j; // (2)
}

特に厄介なケースは次のようになります。

悪い例dart
void f(int? i) {
  i ??= 0;
  var foo = () {
    print(i.isEven); // ERROR
  };
}

この場合、iへの唯一の書き込みはnull以外の値を使用しており、fooが作成される前に行われるため、プロモーションは安全であると人間は判断できますが、フロー解析はそれほど賢くありません

解決策:

繰り返しますが、解決策はローカル変数を作成することです。

良い例dart
void f(int? i) {
  var j = i ?? 0;
  var foo = () {
    print(j.isEven); // OK
  };
}

この解決策は、jの初期値(i ?? 0)により、jはnull許容でない型(int)を持つと推論されるため機能します。jはnull許容でない型を持つため、後で代入されるかどうかに関係なく、jはnull以外の値を持つことは決してありません。

現在のクロージャまたは関数式の外でキャプチャされた書き込み

#

原因: プロモーションしようとしている変数はクロージャまたは関数式の外部で書き込みキャプチャされていますが、この変数の使用は、それをプロモーションしようとしているクロージャまたは関数式の内部です。

悪い例dart
void f(int? i, int? j) {
  var foo = () {
    if (i == null) return;
    print(i.isEven); // ERROR
  };
  var bar = () {
    i = j;
  };
}

フロー解析は、foobarがどのような順序で実行されるかを判断する方法がないと判断します。実際、foobarを呼び出すものを呼び出すため、barfooの実行途中に実行される可能性さえあります。そのため、foo内部でiを完全にプロモーションすることは安全ではありません。

解決策:

最適な解決策はおそらく、ローカル変数を作成することです。

良い例dart
void f(int? i, int? j) {
  var foo = () {
    var i2 = i;
    if (i2 == null) return;
    print(i2.isEven); // OK because i2 is local to this closure.
  };
  var bar = () {
    i = j;
  };
}