跳到主要內容

修復型別提升失敗

型別提升發生在流分析可以健全地確認一個可空型別的變數為非空,並且從那時起它將不會改變時。許多情況會削弱型別的健全性,導致型別提升失敗。

本頁列出了型別提升失敗發生的原因,並提供了修復它們的技巧。要了解更多關於流分析和型別提升的資訊,請檢視理解空安全頁面。

欄位提升不支援的語言版本

#

原因:您正在嘗試提升一個欄位,但欄位提升是受語言版本限制的,而您的程式碼設定的語言版本低於 3.2。

如果您已經在使用 Dart 3.2 或更高版本的 SDK,您的程式碼可能仍然明確地針對較早的語言版本。這可能發生在以下情況:

  • 您的 pubspec.yaml 聲明瞭一個 SDK 約束,其下限低於 3.2,或者
  • 您在檔案頂部有一個 // @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 註釋,或檢查 pubspec.yaml 中是否存在過時的 SDK 約束下限

只有區域性變數可以提升(Dart 3.2 之前)

#

原因:您正在嘗試提升一個屬性,但在 Dart 3.2 之前的版本中只有區域性變數可以提升,並且您正在使用 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 或更高版本

如果您需要繼續使用舊版本,請閱讀其他原因和變通方法

其他原因和變通方法

#

本頁的其餘示例說明了與版本不一致無關的提升失敗原因,包括欄位和區域性變數的失敗,並提供了示例和變通方法。

通常,提升失敗的常見修復方法包括以下一種或多種:

  • 將屬性的值賦值給您需要的非空型別的區域性變數。
  • 新增顯式的空檢查(例如,i == null)。
  • 如果您確定表示式不可能是 null,請使用 !as 作為冗餘檢查

以下是建立區域性變數(可以命名為 i)來儲存 i 值的一個示例:

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

此示例展示了一個例項欄位,但它也可以使用例項 getter、靜態欄位或 getter、頂層變數或 getter,或者this

以下是使用 i! 的一個示例:

良好示例dart
print(i!.isEven);

無法提升 this

#

原因:您正在嘗試提升 this,但目前尚不支援對 this 的型別提升。

一個常見的 this 提升場景是在編寫擴充套件方法時。如果擴充套件方法的on 型別是可空型別,您會希望進行空檢查以檢視 this 是否為 null

示例

不良示例dart
extension on int? {
  int get valueOrZero {
    return this == null ? 0 : this; // ERROR
  }
}

訊息

`this` can't be promoted.

解決方案

建立一個區域性變數來儲存 this 的值,然後執行空檢查。

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

只有私有欄位可以提升

#

原因:您正在嘗試提升一個欄位,但該欄位不是私有的。

程式中的其他庫可能會用 getter 覆蓋公共欄位。因為getter 可能不會返回一個穩定的值,並且編譯器無法知道其他庫正在做什麼,所以非私有欄位無法提升。

示例

不良示例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 可空型別提升為非空型別是不安全的。

示例

不良示例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
    }
  }
}

Getter 無法提升

#

原因:您正在嘗試提升一個 getter,但只有例項欄位可以提升,例項 getter 不可以。

編譯器無法保證 getter 每次都返回相同的結果。由於它們的穩定性無法確認,因此 getter 不安全,無法提升。

示例

不良示例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.

解決方案

將 getter 賦值給區域性變數

良好示例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

外部欄位不會提升,因為它們本質上是外部 getter;它們的實現是來自 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
    }
  }
}

與庫中其他地方的 getter 衝突

#

原因:您正在嘗試提升一個欄位,但同一庫中的另一個類包含一個同名的具體 getter。

示例

不良示例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'.

解決方案:

如果 getter 和欄位相關並且需要共享它們的名稱(例如其中一個覆蓋另一個,如上例所示),那麼您可以透過將值賦給區域性變數來啟用型別提升。

良好示例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 是不安全的:因為欄位和 getter 之間存在覆蓋關係。但是,即使類不相關,衝突的 getter 也會阻止欄位提升。例如:

不良示例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
  }
}

另一個庫可能包含一個將兩個不相關類組合到相同類層次結構中的類,這將導致函式 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 內部生成的非健全的隱式 noSuchMethod 轉發器(也名為 _i)。

編譯器建立 _i 的這個隱式實現,是因為 MockExample 在其宣告中實現 Example 時承諾支援 _i 的 getter,但未能履行該承諾。因此,未定義的 getter 實現由 MocknoSuchMethod 定義處理,這會建立一個同名的隱式 noSuchMethod 轉發器。

此失敗也可能發生在不相關類的欄位之間。

訊息

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

解決方案

定義有問題的 getter,這樣 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
  }
}

該 getter 被宣告為 late,以與 mock 的通常用法保持一致;在不涉及 mock 的場景中,解決此型別提升失敗並不需要將 getter 宣告為 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 從非空 int 降級回可空 int?。人類可以判斷 (2) 處的訪問是安全的,因為沒有程式碼路徑同時包含 (1) 和 (2),但流分析不夠智慧,無法看到這一點,因為它不跟蹤獨立 if 語句中條件之間的關聯。

您可以透過合併兩個 if 語句來解決此問題。

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

在像這樣的直線控制流情況(沒有迴圈)中,流分析在決定是否降級時會考慮賦值的右側。因此,修復此程式碼的另一種方法是將 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 的寫入。但因為它正在向前檢視,它尚未確定賦值右側的型別,因此它不知道保留提升是否安全。為了安全起見,它使提升失效。

解決方案:

您可以透過將空檢查移到迴圈頂部來解決此問題。

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

如果 case 塊帶有標籤,此情況也可能出現在 switch 語句中,因為您可以使用帶標籤的 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;
  }
}

同樣,您可以透過將空檢查移到迴圈頂部來解決此問題。

良好示例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 塊中的哪個時間點,因此它保守地假設異常可能發生在 (1) 和 (2) 之間,當時 i 可能為 null

類似情況可能發生在 tryfinally 塊之間,以及 catchfinally 塊之間。由於實現方式的歷史遺留問題,這些 try/catch/finally 情況不會考慮賦值的右側,這與迴圈中發生的情況類似。

解決方案:

要解決此問題,請確保 catch 塊不依賴於關於在 try 塊內更改的變數狀態的假設。請記住,異常可能在 try 塊執行期間的任何時間發生,可能在 inull 時。

最安全的解決方案是在 catch 塊內部新增空檢查。

良好示例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.
  }
}

或者,如果您確定當 inull 時不會發生異常,只需使用 ! 運算子。

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,因為 Pattern 不是 Comparable 的子型別。(理由是,如果它被提升,那麼您將無法在 Comparable 上使用方法。)請注意,僅僅因為 Pattern 不是 Comparable 的子型別並不意味著 (3) 處的程式碼是死程式碼;o 可能具有一個同時實現 ComparablePattern 的型別——例如 String

解決方案:

一種可能的解決方案是建立一個新的區域性變數,以便原始變數被提升為 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 o2 更改為 var 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
    }
  }
}

另一種有時有效的解決方案是使用更精確的型別。如果第 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 的唯一寫入使用了非空值,並且發生在 foo 建立之前。但是流分析並沒有那麼智慧

解決方案:

同樣,一個解決方案是建立一個區域性變數。

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

此解決方案有效,因為 j 由於其初始值 (i ?? 0) 而被推斷為非空型別 (int)。由於 j 具有非空型別,無論它是否在以後被賦值,j 永遠不會是 null

在當前閉包或函式表示式之外被寫入捕獲

#

原因:您嘗試提升的變數在閉包或函式表示式之外被寫入捕獲,但變數的此用法位於嘗試提升它的閉包或函式表示式內部。

示例

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

流分析推斷無法確定 foobar 可能以何種順序執行;事實上,bar 甚至可能在執行 foo 的過程中(由於 foo 呼叫了某個呼叫 bar 的東西)被執行。因此,在 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;
  };
}