修正型別提升失敗
型別提升發生在流程分析可以可靠地確認具有可空型別的變數不是 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
無法被提升,因為它可能會解析為不合理的隱含 noSuchMethod
轉發器(也命名為 _i
),該轉發器是由編譯器在 MockExample
內部產生的。
編譯器會建立此 _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
,以與通常使用 mocks 的方式一致;在不涉及 mocks 的情況下,不需要宣告 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
語句中條件之間的關聯性。
您可能會透過合併兩個 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
的寫入。但由於它正在向前看,它尚未弄清楚指派右側的型別,因此它不知道保留提升是否安全。為了安全起見,它會使提升失效。
解決方案:
您可以透過將 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
區塊中的何時發生,因此它保守地假設它可能發生在 (1) 和 (2) 之間,當時 i
可能為 null
。
類似的情況可能會發生在 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
可能具有一個型別(例如 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
的寫入使用不可為 null 的值,並且發生在 foo
被建立之前。但是流程分析沒有那麼聰明。
解決方案:
同樣,一個解決方案是建立一個區域變數。
void f(int? i) {
var j = i ?? 0;
var foo = () {
print(j.isEven); // OK
};
}
此解決方案有效,因為 j
被推斷為具有不可為 null 的型別 (int
),因為它的初始值 (i ?? 0
)。因為 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
可能以什麼順序執行;事實上,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.6.0。頁面最後更新時間:2024-12-10。 檢視原始碼 或 回報問題。