型プロモーションの失敗の修正
- フィールドプロモーションでサポートされていない言語バージョン
- (Dart 3.2 より前では) ローカル変数のみをプロモーションできます
- その他の原因と回避策
- プロモーションできないもの
- プライベートフィールドのみをプロモーションできます
- final フィールドのみをプロモーションできます
- ゲッターはプロモーションできません
- 外部フィールドはプロモーションできません
- ライブラリの他の場所でゲッターと競合しています
- ライブラリの他の場所でプロモーションできないフィールドと競合しています
- 暗黙的な noSuchMethod フォワーダーと競合しています
- プロモーション後に書き込まれている可能性があります
- 前のループ反復で書き込まれている可能性があります
- try ブロックで書き込みが行われた可能性のある catch ブロック内
- サブタイプミスマッチ
- ローカル関数によってキャプチャされた書き込み
- 現在のクロージャまたは関数式の外で書き込まれています
- 現在のクロージャまたは関数式の外でキャプチャされた書き込み
型プロモーション は、フロー解析によって、nullable 型 の変数が null でないこと、そしてその時点以降変更されないことが確実に確認できる場合に発生します。多くの状況で型の堅牢性が弱まり、型プロモーションが失敗する原因となります。
このページでは、型プロモーションの失敗が発生する理由を、修正方法のヒントとともにリストしています。フロー解析と型プロモーションの詳細については、Null セーフティの理解 ページをご覧ください。
フィールドプロモーションでサポートされていない言語バージョン
#原因: フィールドのプロモーションを試行していますが、フィールドプロモーションは言語バージョンに依存しており、コードは3.2より前の言語バージョンに設定されています。
SDK バージョン >= Dart 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
コメント、または 古い SDK 制約の下限 を持つ pubspec.yaml
を確認してください。
(Dart 3.2 より前では) ローカル変数のみをプロモーションできます
#原因: プロパティのプロモーションを試行していますが、3.2 より前の Dart バージョンではローカル変数のみをプロモーションでき、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 以降にアップグレードしてください。
古いバージョンを引き続き使用する必要がある場合は、その他の原因と回避策 を参照してください。
その他の原因と回避策
#このページの残りの例では、フィールドとローカル変数の両方のプロモーションの失敗の理由を、バージョンの一貫性の問題とは無関係に、例と回避策とともに説明しています。
一般的に、プロモーションの失敗に対する通常の修正は、以下のいずれか、または複数です。
- 必要な非 nullable 型を持つローカル変数にプロパティの値を代入します。
- 明示的な null チェックを追加します (例:
i == null
)。 - 式が
null
ではないと確信している場合は、冗長なチェックとして!
またはas
を使用します。
i
の値を保持するローカル変数 (i
という名前を付けることができます) を作成する例を以下に示します。
class C {
int? i;
void f() {
final i = this.i;
if (i == null) return;
print(i.isEven);
}
}
この例はインスタンスフィールドを示していますが、代わりにインスタンスゲッター、静的フィールドまたはゲッター、トップレベル変数またはゲッター、またはthis
を使用できます。
そして、i!
を使用した例を以下に示します。
print(i!.isEven);
this
をプロモーションできない
#原因: this
をプロモーションしようとしていますが、this
の型プロモーションはまだサポートされていません。
拡張メソッド を記述する場合、一般的な this
プロモーションのシナリオの1つは、拡張メソッドのon
型 が nullable 型である場合に、this
が null
であるかどうかを確認する 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;
}
}
プライベートフィールドのみをプロモーションできます
#原因: フィールドのプロモーションを試行していますが、そのフィールドはプライベートではありません。
プログラムの他のライブラリが、ゲッターを使用してパブリックフィールドをオーバーライドすることは可能です。ゲッターは安定した値を返さない可能性があるため、コンパイラは他のライブラリが何をしているかを知ることができないため、非プライベートフィールドはプロモーションできません。
例
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 の nullable 型を非 nullable 型にプロモーションすることは安全ではありません。
例
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
}
}
}
ゲッターはプロモーションできません
#原因: ゲッターのプロモーションを試行していますが、プロモーションできるのはインスタンスフィールドだけであり、インスタンスゲッターではありません。
コンパイラは、ゲッターが毎回同じ結果を返すことを保証する方法がありません。安定性が確認できないため、ゲッターはプロモーションできません。
例
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.
解決策
ゲッターをローカル変数に代入します。
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 の外部のコードであるため、外部フィールドが呼び出されるたびに同じ値を返すという保証はコンパイラにはありません。
例
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
}
}
}
ライブラリの他の場所でゲッターと競合しています
#原因: フィールドのプロモーションを試行していますが、同じライブラリの別のクラスに、同じ名前の具体的なゲッターが含まれています。
例
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'.
解決策:
ゲッターとフィールドが関連していて、名前を共有する必要がある場合 (上の例のように、一方のゲッターがもう一方をオーバーライドする場合など)、ローカル変数に値を代入することで型プロモーションを有効にできます。
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
をプロモーションすることが安全ではない理由が明確です。フィールドとゲッターの間にオーバーライドの関係があるためです。ただし、クラスが関連していない場合でも、競合するゲッターがあるとフィールドのプロモーションは妨げられます。例:
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
内で生成する(_i
という名前の)安全ではない暗黙的なnoSuchMethod
フォワーダーに解決される可能性があるため、プロモーションできません。
コンパイラは、MockExample
が宣言でExample
を実装する際に_i
のゲッターをサポートすることを約束しているが、その約束を果たしていないため、_i
のこの暗黙的な実装を作成します。そのため、未定義のゲッター実装はMock
のnoSuchMethod
定義によって処理され、同じ名前の暗黙的なnoSuchMethod
フォワーダーが作成されます。
関連のないクラスのフィールド間でも、このエラーが発生する可能性があります。
メッセージ
'_i' couldn't be promoted because there is a conflicting noSuchMethod forwarder in class 'MockExample'.
解決策
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
}
}
ゲッターは、モックの一般的な使用方法と一致するようにlate
と宣言されています。モックを含まないシナリオでは、この型プロモーションエラーを解決するために、ゲッターを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;
}
}
ラベル付きswitch
文を使用してループを作成できるため、case
ブロックにラベルがある場合、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
のサブタイプではないためです。(その理由は、プロモーションされた場合、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
}
}
}
もう1つの解決策は、より正確な型を使用できる場合に機能します。行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
)により、j
は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;
};
}
特に記載がない限り、このサイトのドキュメントはDart 3.5.3を反映しています。ページ最終更新日: 2024年6月21日。 ソースを表示 または 問題を報告する。