型推論の失敗の修正
型昇格は、フロー解析が null許容型 の変数がnullではないことを論理的に確認でき、それ以降変更されないことを確認できる場合に発生します。多くの状況が型の健全性を損ない、型昇格を失敗させる可能性があります。
このページでは、型昇格が失敗する理由と、それを修正するためのヒントをリストアップしています。フロー解析と型昇格の詳細については、「Null安全性について」ページをご覧ください。
フィールド昇格でサポートされない言語バージョン
#原因: フィールドを昇格しようとしていますが、フィールド昇格は言語バージョンによってサポートされており、コードが3.2より前の言語バージョンに設定されています。
SDKバージョンが3.2以上の場合でも、コードが明示的に古い言語バージョンを対象としている可能性があります。これは、次のいずれかの理由で発生する可能性があります。
- pubspec.yamlが3.2未満の下限を持つSDK制約を宣言している、または
- ファイルの先頭に // @dart=versionコメントがあり、versionが3.2未満である。
例
// @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 コメント、または pubspec.yaml の古いSDK制約の下限を確認してください。
ローカル変数のみ昇格可能(Dart 3.2より前)
#原因: プロパティを昇格しようとしていますが、Dart 3.2より前のバージョンではローカル変数のみが昇格可能であり、あなたは3.2より前のバージョンを使用しています。
例
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以降にアップグレードしてください。
古いバージョンを使い続ける必要がある場合は、「その他の原因と回避策」をお読みください。
その他の原因と回避策
#このページに残りの例は、フィールドとローカル変数の両方の昇格失敗の理由を、バージョン間の不一致とは無関係なものとして、例と回避策とともに文書化しています。
一般的に、昇格失敗の通常の修正方法は、次のいずれかまたは複数です。
- プロパティの値を、必要な非null許容型を持つローカル変数に代入します。
- 明示的なnullチェックを追加します(例: i == null)。
- 式がnullになり得ないと確信している場合は、!またはasを冗長チェックとして使用します。
ここでは、iの値を持つローカル変数(iと名前を付けることができます)を作成する例を示します。
class C {
  int? i;
  void f() {
    final i = this.i;
    if (i == null) return;
    print(i.isEven);
  }
}この例はインスタンスフィールドを特徴としていますが、代わりにインスタンスgetter、静的フィールドまたはgetter、トップレベル変数またはgetter、またはthisを使用することもできます。
そして、i!を使用する例を次に示します。
print(i!.isEven);thisを昇格できません
#原因: thisを昇格しようとしていますが、thisの型昇格はまだサポートされていません。
一般的なthis昇格シナリオは、拡張メソッドを記述する際です。拡張メソッドのon型がnull許容型である場合、nullチェックを行ってthisがnullかどうかを確認したい場合があります。
例
extension on int? {
  int get valueOrZero {
    return this == null ? 0 : this; // ERROR
  }
}メッセージ
`this` can't be promoted.解答
thisの値を保持するローカル変数を作成してから、nullチェックを実行してください。
extension on int? {
  int get valueOrZero {
    final self = this;
    return self == null ? 0 : self;
  }
}プライベートフィールドのみ昇格可能
#原因: フィールドを昇格しようとしていますが、フィールドはプライベートではありません。
プログラム内の他のライブラリがパブリックフィールドをgetterでオーバーライドする可能性があります。 getterは安定した値を返さない場合があるため、コンパイラは他のライブラリが何をしているかを知ることができないため、非プライベートフィールドは昇格できません。
例
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.解答
フィールドをプライベートにすると、外部ライブラリがその値をオーバーライドできないことをコンパイラが確実に把握できるため、昇格しても安全です。
class Example {
  final int? _value;
  Example(this._value);
}
void test(Example x) {
  if (x._value != null) {
    print(x._value + 1);
  }
}finalフィールドのみ昇格可能
#原因: フィールドを昇格しようとしていますが、フィールドはfinalではありません。
コンパイラにとって、非finalフィールドは、テストされた時点と使用される時点の間にいつでも変更される可能性があります。そのため、コンパイラが非final null許容型を非null許容型に昇格するのは安全ではありません。
例
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にします
class Example {
  final int? _immutablePrivateField;
  Example(this._immutablePrivateField);
  void f() {
    if (_immutablePrivateField != null) {
      int i = _immutablePrivateField; // OK
    }
  }
}getterは昇格できません
#原因: getterを昇格しようとしていますが、インスタンスフィールドのみが昇格可能であり、インスタンスgetterは昇格できません。
コンパイラは、getterが毎回同じ結果を返すことを保証する方法がありません。それらの安定性を確認できないため、getterは昇格しても安全ではありません。
例
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.解答
getterをローカル変数に代入します
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がマークされています。
外部フィールドは、基本的に外部getterであるため昇格しません。それらの実装は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.解答
外部フィールドの値をローカル変数に代入します
class Example {
  external final int? _externalField;
  void f() {
    final i = _externalField;
    if (i != null) {
      print(i.isEven); // OK
    }
  }
}ライブラリ内の他の場所にあるgetterとの競合
#原因: フィールドを昇格しようとしていますが、同じライブラリ内の別のクラスに同じ名前の具体的なgetterが含まれています。
例
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'.解答:
getterとフィールドが関連しており、名前を共有する必要がある場合(たとえば、一方が他方をオーバーライドする場合など)、ローカル変数に値を代入することで型昇格を有効にできます。
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
  }
}無関係なクラスに関する注意
#上記の例では、getterがオーバーライドされているため、フィールド_overriddenを昇格するのが安全でない理由が明確ですが、getterとのオーバーライド関係があります。ただし、競合するgetterは、クラスが無関係であってもフィールド昇格を防ぎます。たとえば、
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にディスパッチされる原因となります。たとえば、
class Surprise extends Unrelated implements Example {}
void main() {
  f(Surprise());
}解答
フィールドと競合するエンティティが実際に関連していない場合は、名前を変更することで問題を回避できます。
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
  }
}ライブラリ内の他の場所にある昇格不可能なフィールドとの競合
#原因: フィールドを昇格しようとしていますが、同じライブラリ内の別のクラスに、昇格不可能な同じ名前のフィールドが含まれています(このページでリストされている他の理由のいずれか)。
例
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ローカル変数に値を代入することで型昇格を有効にできます。
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が呼び出しごとに安定した値を返さない保証がないため、これは健全ではありません。
例
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内で生成する(同じ名前の)サウンドでない暗黙のnoSuchMethodフォワーダーに解決される可能性があるため、昇格できません。
コンパイラは、MockExampleが宣言でExampleを実装するときに_iのgetterをサポートすることを約束しますが、その約束を果たさないため、この暗黙の実装を生成します。したがって、未定義のgetter実装はMockのnoSuchMethod定義によって処理され、同じ名前の暗黙のnoSuchMethodフォワーダーが作成されます。
この失敗は、無関係なクラスのフィールド間でも発生する可能性があります。
メッセージ
'_i' couldn't be promoted because there is a conflicting noSuchMethod forwarder in class 'MockExample'.解答
問題のgetterを定義して、noSuchMethodがその実装を暗黙的に処理する必要がないようにします。
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
  }
}getterは、モックの一般的な使用方法と一致するようにlateとして宣言されています。モックに関係のないシナリオでこの型昇格の失敗を解決するために、getterをlateとして宣言する必要はありません。
昇格後に書き込まれた可能性
#原因: プロモーション後に書き込まれた可能性のある変数を昇格しようとしています。
例
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文を結合することで問題を修正できる場合があります。
void f(bool b, int? i, int? j) {
  if (i == null) return;
  if (b) {
    i = j;
  } else {
    print(i.isEven);
  }
}これらの(ループのない)直線的な制御フローケースでは、フロー解析はデモートを決定する際に代入の右辺を考慮します。その結果、このコードを修正するもう1つの方法は、jの型をintに変更することです。
void f(bool b, int? i, int j) {
  if (i == null) return;
  if (b) {
    i = j;
  }
  if (!b) {
    print(i.isEven);
  }
}前のループイテレーションで書き込まれた可能性
#原因: ループの前のイテレーションで書き込まれた可能性のあるものを昇格しようとしており、その結果、昇格が無効になっています。
例
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チェックを移動することで、この問題を修正できます。
void f(Link? p) {
  while (p != null) {
    print(p.value);
    p = p.next;
  }
}caseブロックにラベルがある場合、`switch`ステートメントでも同様の状況が発生する可能性があります。これは、ラベル付き`switch`ステートメントを使用してループを構築できるためです。
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チェックを移動することで問題を修正できます。
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ブロックにある場合。
例
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)の間で発生したと仮定するからです。
同様の状況がtryとfinallyブロックの間、およびcatchとfinallyブロックの間で発生する可能性があります。実装方法の履歴的なアーティファクトのため、これらのtry/catch/finally状況では、ループで発生することと同様に、代入の右辺が考慮されません。
解答:
問題を修正するには、catchブロックが、tryブロック内で変更される変数の状態に関する仮定に依存しないようにしてください。例外はtryブロックのいつでも発生する可能性があり、その時点ではiがnullである可能性があることを忘れないでください。
最も安全な解決策は、catchブロック内にnullチェックを追加することです。
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の間例外が発生しないと確信している場合は、!演算子を使用するだけです。
try {
  // ···
} catch (e) {
  print(i!.isEven); // (3) OK because of the `!`.
}サブタイプ不一致
#原因: 変数を、変数の現在の昇格された型(または昇格の試行時にサブタイプではなかった型)のサブタイプではない型に昇格しようとしています。
例
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には昇格しません。なぜなら、PatternはComparableのサブタイプではないからです。(PatternがComparableのサブタイプでないからといって、(3)のコードが無効になるわけではありません。oは、ComparableとPatternの両方を実装する型、例えばStringである可能性があります。)
解答:
考えられる解決策の1つは、新しいローカル変数を作成することです。これにより、元の変数はComparableに昇格し、新しい変数はPatternに昇格します。
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 o2をvar o2に変更したくなるかもしれません。その変更はo2にComparableという型を与えるため、オブジェクトがPatternに昇格できないという問題が再発します。
冗長な型チェックの方が良い解決策かもしれません。
void f(Object o) {
  if (o is Comparable /* (1) */ ) {
    if (o is Pattern /* (2) */ ) {
      print((o as Pattern).matchAsPrefix('foo')); // (3) OK
    }
  }
}時折機能する別の解決策は、より正確な型を使用できる場合です。行3が文字列のみを気にする場合、型チェックでStringを使用できます。StringはComparableのサブタイプであるため、昇格は機能します。
void f(Object o) {
  if (o is Comparable /* (1) */ ) {
    if (o is String /* (2) */ ) {
      print(o.matchAsPrefix('foo')); // (3) OK
    }
  }
}ローカル関数によってキャプチャされた書き込み
#原因: 変数がローカル関数または関数式によって書き込みキャプチャされています。
例
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を昇格することはもはや安全ではありません。ループと同様に、このデモートは代入の右辺の型に関係なく発生します。
解答:
場合によっては、昇格が書き込みキャプチャ前に行われるようにロジックを再構成することが可能です。
void f(int? i, int? j) {
  if (i == null) return; // (1)
  // ... Additional code ...
  print(i.isEven); // (2) OK
  var foo = () {
    i = j;
  };
  // ... Use foo ...
}別のオプションは、ローカル変数を作成することです。これにより、書き込みキャプチャされなくなります。
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.
}または冗長チェックを行うことができます。
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.
}現在のクロージャまたは関数式外で書き込まれた
#原因: 変数はクロージャまたは関数式の外で書き込まれており、型昇格の場所はそのクロージャまたは関数式の中にあります。
例
void f(int? i, int? j) {
  if (i == null) return;
  var foo = () {
    print(i.isEven); // (1) ERROR
  };
  i = j;             // (2)
}フロー解析は、fooがいつ呼び出されるかを判断する手段がないと推論するため、(2)の代入の後で呼び出される可能性があり、その結果、昇格はもはや有効ではない可能性があります。ループと同様に、このデモートは代入の右辺の型に関係なく発生します。
解答:
解決策は、ローカル変数を作成することです。
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)
}例
特に厄介なケースは次のようになります。
void f(int? i) {
  i ??= 0;
  var foo = () {
    print(i.isEven); // ERROR
  };
}この場合、人間は、iへの唯一の書き込みが非null値を使用し、fooが作成される前に発生するため、昇格は安全であると判断できます。しかし、フロー解析はそれほど賢くありません。
解答:
ここでも、解決策はローカル変数を作成することです。
void f(int? i) {
  var j = i ?? 0;
  var foo = () {
    print(j.isEven); // OK
  };
}この解決策は、jの初期値(i ?? 0)により、非null許容型(int)と推論されるため機能します。jは非null許容型であるため、後で代入されるかどうかに関係なく、jは決してnull値を持つことができません。
現在のクロージャまたは関数式外でキャプチャされた書き込み
#原因: 昇格しようとしている変数は、クロージャまたは関数式の外で書き込みキャプチャされていますが、変数のこの使用法は、それを昇格しようとしているクロージャまたは関数式の中にあります。
例
void f(int? i, int? j) {
  var foo = () {
    if (i == null) return;
    print(i.isEven); // ERROR
  };
  var bar = () {
    i = j;
  };
}フロー解析は、fooとbarが実行される順序を判断する手段がないと推論します。実際、fooがbarを呼び出すものを呼び出すことによって、barがfooの実行途中で実行されるさえあります。そのため、foo内でiを昇格することは安全ではありません。
解答:
最善の解決策はおそらくローカル変数を作成することです。
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;
  };
}