跳至主要內容

修正類型提升失敗問題

類型提升發生在流程分析可以可靠地確認具有可空類型的變數不是 null,並且從那時起不會改變的情況。許多情況可能會削弱類型的健全性,導致類型提升失敗。

本頁列出了類型提升失敗的原因,並提供如何修正這些問題的提示。若要深入了解流程分析和類型提升,請查看了解空值安全頁面。

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

#

原因:您嘗試提升欄位,但欄位提升是語言版本化的,而您的程式碼設定為 3.2 之前的語言版本。

如果您已經使用 SDK 版本 >= Dart 3.2,您的程式碼可能仍然明確地以較早的語言版本為目標。這可能是因為

  • 您的pubspec.yaml宣告了 SDK 限制,其下限低於 3.2,或者
  • 您在檔案頂端有一個 // @dart=version 註解,其中 version 低於 3.2。

範例

baddart
// @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 之前的版本。

範例

baddart
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 值的範例

gooddart
class C {
  int? i;
  void f() {
    final i = this.i;
    if (i == null) return;
    print(i.isEven);
  }
}

此範例以實例欄位為特色,但也可以改用實例 getter、靜態欄位或 getter、頂層變數或 getter,或this

以下是使用 i! 的範例

gooddart
print(i!.isEven);

無法提升 this

#

原因:您嘗試提升 this,但尚不支援 this 的類型提升。

一個常見的 this 提升情境是撰寫擴充方法時。如果擴充方法的 on 類型是可空類型,您會想要執行空值檢查,以查看 this 是否為 null

範例

baddart
extension on int? {
  int get valueOrZero {
    return this == null ? 0 : this; // ERROR
  }
}

訊息

`this` can't be promoted.

解決方案

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

gooddart
extension on int? {
  int get valueOrZero {
    final self = this;
    return self == null ? 0 : self;
  }
}

只有私有欄位可以提升

#

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

您的程式中的其他程式庫可能會使用 getter 覆寫公開欄位。由於getter 可能不會傳回穩定的值,而且編譯器無法知道其他程式庫正在做什麼,因此無法提升非私有欄位。

範例

baddart
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.

解決方案

將欄位設為私有,可讓編譯器確定沒有外部程式庫可能會覆寫其值,因此提升是安全的。

gooddart
class Example {
  final int? _value;
  Example(this._value);
}

void test(Example x) {
  if (x._value != null) {
    print(x._value + 1);
  }
}

只有 final 欄位可以提升

#

原因:您嘗試提升欄位,但該欄位不是 final。

對於編譯器而言,原則上,非 final 欄位可能會在測試時間和使用時間之間的任何時間修改。因此,編譯器將非 final 可空類型提升為非可空類型是不安全的。

範例

baddart
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

gooddart
class Example {
  final int? _immutablePrivateField;
  Example(this._immutablePrivateField);

  void f() {
    if (_immutablePrivateField != null) {
      int i = _immutablePrivateField; // OK
    }
  }
}

Getter 無法提升

#

原因:您嘗試提升 getter,但只有實例欄位可以提升,實例 getter 則不行。

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

範例

baddart
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 指派給區域變數

gooddart
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 外部的程式碼,因此編譯器無法保證外部欄位每次呼叫都會傳回相同的值。

範例

baddart
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.

解決方案

將外部欄位的值指派給區域變數

gooddart
class Example {
  external final int? _externalField;

  void f() {
    final i = _externalField;
    if (i != null) {
      print(i.isEven); // OK
    }
  }
}

與程式庫其他地方的 getter 衝突

#

原因:您嘗試提升欄位,但同一個程式庫中的另一個類別包含具有相同名稱的具體 getter。

範例

baddart
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 和欄位相關且需要共用其名稱 (例如,當其中一個覆寫另一個時,如上例所示),則您可以透過將值指派給區域變數來啟用類型提升

gooddart
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 也會阻止欄位提升。例如

baddart
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。例如

baddart
class Surprise extends Unrelated implements Example {}

void main() {
  f(Surprise());
}

解決方案

如果欄位和衝突實體確實不相關,您可以透過為它們指定不同的名稱來解決問題

gooddart
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
  }
}

與程式庫其他地方無法提升的欄位衝突

#

原因:您嘗試提升欄位,但同一個程式庫中的另一個類別包含具有相同名稱的欄位,該欄位無法提升 (由於本頁列出的任何其他原因)。

範例

baddart
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 區域變數來啟用類型提升

gooddart
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 會從一個調用到下一個調用傳回穩定的值。

範例

baddart
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 不必隱含地處理其實作

gooddart
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 並不是解決不涉及模擬物件的情境中的類型提升失敗所必需的。

可能在提升後寫入

#

原因:您嘗試提升的變數可能自提升後已被寫入。

範例

baddart
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 陳述式來修正問題

gooddart
void f(bool b, int? i, int? j) {
  if (i == null) return;
  if (b) {
    i = j;
  } else {
    print(i.isEven);
  }
}

在像這樣直線控制流程的情況下 (沒有迴圈),流程分析在決定是否降級時會考慮指派的右側。因此,修正此程式碼的另一種方法是將 j 的類型變更為 int

gooddart
void f(bool b, int? i, int j) {
  if (i == null) return;
  if (b) {
    i = j;
  }
  if (!b) {
    print(i.isEven);
  }
}

可能在先前的迴圈迭代中寫入

#

原因:您嘗試提升的內容可能在迴圈的先前迭代中已被寫入,因此提升已失效。

範例

baddart
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。但是由於它是向前看的,因此尚未弄清楚指派右側的類型,因此它不知道保留提升是否安全。為了安全起見,它會使提升失效。

解決方案:

您可以透過將空值檢查移至迴圈頂端來修正此問題

gooddart
void f(Link? p) {
  while (p != null) {
    print(p.value);
    p = p.next;
  }
}

如果 case 區塊具有標籤,則這種情況也可能在 switch 陳述式中發生,因為您可以使用帶標籤的 switch 陳述式來建構迴圈

baddart
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;
  }
}

同樣,您可以透過將空值檢查移至迴圈頂端來修正問題

gooddart
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 區塊中。

範例

baddart
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 區塊內部新增空值檢查

gooddart
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 `!`.
}

子類型不符

#

原因:您嘗試提升為的類型不是變數目前提升類型的子類型 (或在嘗試提升時不是子類型)。

範例

baddart
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) — 實作 ComparablePattern

解決方案:

一種可能的解決方案是建立一個新的區域變數,以便原始變數提升為 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 的問題。

多餘的類型檢查可能是更好的解決方案

gooddart
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 的子類型,因此提升有效

gooddart
void f(Object o) {
  if (o is Comparable /* (1) */ ) {
    if (o is String /* (2) */ ) {
      print(o.matchAsPrefix('foo')); // (3) OK
    }
  }
}

寫入被區域函式捕獲

#

原因:變數已被區域函式或函式表達式寫入捕獲。

範例

baddart
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 已不再安全。與迴圈一樣,無論指派右側的類型如何,都會發生此降級。

解決方案:

有時可以重組邏輯,使提升在寫入捕獲之前

gooddart
void f(int? i, int? j) {
  if (i == null) return; // (1)
  // ... Additional code ...
  print(i.isEven); // (2) OK
  var foo = () {
    i = j;
  };
  // ... Use foo ...
}

另一種選擇是建立區域變數,使其不被寫入捕獲

gooddart
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.
}

寫入在目前的閉包或函式表達式之外

#

原因:變數在閉包或函式表達式之外被寫入,而類型提升位置在閉包或函式表達式內部。

範例

baddart
void f(int? i, int? j) {
  if (i == null) return;
  var foo = () {
    print(i.isEven); // (1) ERROR
  };
  i = j;             // (2)
}

流程分析推斷,沒有辦法確定 foo 可能在何時被呼叫,因此它可能會在 (2) 處的指派之後被呼叫,因此提升可能不再有效。與迴圈一樣,無論指派右側的類型如何,都會發生此降級。

解決方案:

一種解決方案是建立區域變數

gooddart
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)
}

範例

一個特別棘手的案例如下所示

baddart
void f(int? i) {
  i ??= 0;
  var foo = () {
    print(i.isEven); // ERROR
  };
}

在這種情況下,人類可以看到提升是安全的,因為對 i 的唯一寫入使用非空值,並且在建立 foo 之前發生。但是流程分析沒有那麼聰明

解決方案:

同樣,一種解決方案是建立區域變數

gooddart
void f(int? i) {
  var j = i ?? 0;
  var foo = () {
    print(j.isEven); // OK
  };
}

此解決方案有效,因為 j 被推斷為具有非可空類型 (int),這是由於其初始值 (i ?? 0)。由於 j 具有非可空類型,因此無論是否稍後指派,j 永遠不可能具有空值。

寫入在目前的閉包或函式表達式之外被捕獲

#

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

範例

baddart
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 是不安全的。

解決方案:

最好的解決方案可能是建立區域變數

gooddart
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;
  };
}