修正類型提升失敗問題
類型提升發生在流程分析可以可靠地確認具有可空類型的變數不是 null,並且從那時起不會改變的情況。許多情況可能會削弱類型的健全性,導致類型提升失敗。
本頁列出了類型提升失敗的原因,並提供如何修正這些問題的提示。若要深入了解流程分析和類型提升,請查看了解空值安全頁面。
欄位提升不支援的語言版本
#原因:您嘗試提升欄位,但欄位提升是語言版本化的,而您的程式碼設定為 3.2 之前的語言版本。
如果您已經使用 SDK 版本 >= Dart 3.2,您的程式碼可能仍然明確地以較早的語言版本為目標。這可能是因為
- 您的
pubspec.yaml
宣告了 SDK 限制,其下限低於 3.2,或者 - 您在檔案頂端有一個
// @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 或更新版本。
如果您需要繼續使用舊版本,請閱讀其他原因和解決方法
其他原因和解決方法
#本頁剩餘的範例記錄了與版本不一致無關的提升失敗原因,包括欄位和區域變數失敗,並提供範例和解決方法。
一般而言,提升失敗的常見修正方法是以下一或多項
- 將屬性的值指派給具有您需要的非可空類型的區域變數。
- 新增明確的空值檢查 (例如,
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
類型是可空類型,您會想要執行空值檢查,以查看 this
是否為 null
範例
extension on int? {
int get valueOrZero {
return this == null ? 0 : this; // ERROR
}
}
訊息
`this` can't be promoted.
解決方案
建立一個區域變數來保存 this
的值,然後執行空值檢查。
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 可空類型提升為非可空類型是不安全的。
範例
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
}
}
關於不相關類別的注意事項
#請注意,在上面的範例中,很明顯為什麼提升欄位 _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
}
}
另一個程式庫可能包含一個類別,該類別將兩個不相關的類別組合到同一個類別階層中,這將導致函數 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
轉發器 (也命名為 _i
)。
編譯器會建立 _i
的這個隱含實作,因為 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
從非可空 int
降級回可空 int?
。人類可以判斷 (2) 處的存取是安全的,因為沒有包含 (1) 和 (2) 的程式碼路徑,但流程分析不夠聰明,無法看到這一點,因為它不追蹤單獨的 if
陳述式中條件之間的關聯性。
您可以透過合併兩個 if
陳述式來修正問題
void f(bool b, int? i, int? j) {
if (i == null) return;
if (b) {
i = j;
} else {
print(i.isEven);
}
}
在像這樣直線控制流程的情況下 (沒有迴圈),流程分析在決定是否降級時會考慮指派的右側。因此,修正此程式碼的另一種方法是將 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
。但是由於它是向前看的,因此尚未弄清楚指派右側的類型,因此它不知道保留提升是否安全。為了安全起見,它會使提升失效。
解決方案:
您可以透過將空值檢查移至迴圈頂端來修正此問題
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;
}
}
同樣,您可以透過將空值檢查移至迴圈頂端來修正問題
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
區塊中的何時發生,因此它保守地假設它可能發生在 (1) 和 (2) 之間,當時 i
可能為 null
。
類似的情況可能發生在 try
和 finally
區塊之間,以及 catch
和 finally
區塊之間。由於實作方式的歷史遺留問題,這些 try
/catch
/finally
情況不考慮指派的右側,這與迴圈中發生的情況類似。
解決方案:
若要修正問題,請確保 catch
區塊不依賴於對在 try
區塊內部變更的變數狀態的假設。請記住,異常可能在 try
區塊期間的任何時間發生,可能在 i
為 null
時發生。
最安全的解決方案是在 catch
區塊內部新增空值檢查
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
可能具有一種類型 (例如 String
) — 實作 Comparable
和 Pattern
。
解決方案:
一種可能的解決方案是建立一個新的區域變數,以便原始變數提升為 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
的唯一寫入使用非空值,並且在建立 foo
之前發生。但是流程分析沒有那麼聰明。
解決方案:
同樣,一種解決方案是建立區域變數
void f(int? i) {
var j = i ?? 0;
var foo = () {
print(j.isEven); // OK
};
}
此解決方案有效,因為 j
被推斷為具有非可空類型 (int
),這是由於其初始值 (i ?? 0
)。由於 j
具有非可空類型,因此無論是否稍後指派,j
永遠不可能具有空值。
寫入在目前的閉包或函式表達式之外被捕獲
#原因:您嘗試提升的變數在閉包或函式表達式之外被寫入捕獲,但此變數的使用在嘗試提升它的閉包或函式表達式內部。
範例
void f(int? i, int? j) {
var foo = () {
if (i == null) return;
print(i.isEven); // ERROR
};
var bar = () {
i = j;
};
}
流程分析推斷,無法判斷 foo
和 bar
可能以何種順序執行;事實上,bar
甚至可能在執行 foo
的一半時執行 (由於 foo
呼叫了呼叫 bar
的內容)。因此,在 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.7.1。頁面上次更新於 2025-02-12。 查看原始碼 或 回報問題。