內容

修正型別提升失敗

類型提升發生在流程分析可以合理確認 可為空類型 的值不為空,且其值從此點開始不會改變。許多情況會削弱類型的健全性,導致類型提升失敗。

此頁面列出類型提升失敗發生的原因,並提供如何修復的提示。如需深入了解,請查看 了解 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 之前)

#

原因:您嘗試提升屬性,但只有區域變數可以在早於 3.2 的 Dart 版本中提升,而您使用的版本早於 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

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

其他原因和解決方法

#

此頁面上的其他範例說明了與版本不一致無關的提升失敗原因,包括欄位和區域變數失敗,並提供範例和解決方法。

一般來說,提升失敗的常見解決方法包括下列一項或多項

  • 將屬性的值指定給您需要的非可為 Null 類型的區域變數。
  • 加入明確的 Null 檢查(例如,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類型是可為 Null 的類型,您會想要執行 Null 檢查以查看this是否為 Null

範例

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

訊息

`this` can't be promoted.

解決方案

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

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

只有私有欄位可以提升

#

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

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

範例

baddart
class C {
  final int? n;
  C(this.n);
}

test(C c) {
  if (c.n != null) {
    print(c.n + 1); // ERROR
  }
}

訊息

'n' refers to a public property so it couldn’t be promoted.

解決方案

將欄位設為私有,讓編譯器確信沒有外部函式庫可以覆寫其值,因此可以安全地提升。

gooddart
class C {
  final int? _n;
  C(this._n);
}

test(C c) {
  if (c._n != null) {
    print(c._n + 1); // OK
  }
}

只有 final 欄位可以提升

#

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

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

範例

baddart
class C {
  int? _mutablePrivateField;
  Example(this._mutablePrivateField);

  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);

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

無法提升 getter

#

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

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

範例

baddart
import 'dart:math';

abstract class C {
  int? get _i => Random().nextBool() ? 123 : null;
}

f(C c) {
  if (c._i != null) {
    print(c._i.isEven); // ERROR
  }
}

訊息

'_i' refers to a getter so it couldn’t be promoted.

解決方案

將 getter 指定給一個區域變數

gooddart
import 'dart:math';

abstract class C {
  int? get _i => Random().nextBool() ? 123 : null;
}

f(C c) {
  final i = c._i;
  if (i != null) {
    print(i.isEven); // OK
  }
}

無法提升外部欄位

#

原因:您嘗試提升欄位,但該欄位標記為 external

外部欄位不會提升,因為它們基本上是外部 getter;它們的實作是 Dart 外部的程式碼,因此編譯器無法保證外部欄位每次呼叫時都會傳回相同的值。

範例

baddart
class C {
  external final int? _externalField;
  C(this._externalField);

  f() {
    if (_externalField != null) {
      print(_externalField.isEven); // ERROR
    }
  }
}

訊息

'externalField' refers to an external field so it couldn’t be promoted.

解決方案

將外部欄位的值指定給局部變數

gooddart
class C {
  external final int? _externalField;
  C(this._externalField);

  f() {
    final i = this._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;
}

f(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;
}

f(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;
}

f(Example x) {
  if (x._i != null) {
    int i = x._i; // ERROR
  }
}

另一個函式庫可能包含一個類別,將兩個不相關的類別組合到同一個類別階層中,這會導致函式 f 中對 x._i 的參照傳送至 Unrelated._i。例如

baddart
class Surprise extends Unrelated implements Example {}

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

解決方案

如果欄位和衝突的實體真的不相關,您可以透過給它們不同的名稱來解決這個問題

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

class Unrelated {
  int? get _j => Random().nextBool() ? 1 : null;
}

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

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

解決方案

如果欄位實際上相關,且需要共用一個名稱,您可以將值指定給最終的局部變數來啟用類型提升,以進行提升

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

class Override implements Example {
  @override
  int? _overridden;
}

f(Example x) {
  final i = x._overridden;
  if (i != null) {
    print(i.isEven); // ERROR
  }
}

如果欄位不相關,請直接變更其中一個欄位的名稱,讓它們不會衝突。請閱讀 關於不相關類別的注意事項

與隱式 noSuchMethod 轉發器的衝突

#

原因:您嘗試提升一個為私有且最終的欄位,但同一個函式庫中的另一個類別包含一個 隱式 noSuchMethod 轉發器,其名稱與欄位相同。

這是不正確的,因為無法保證 noSuchMethod 會在一次呼叫到下一次呼叫時傳回穩定的值。

範例

baddart
import 'package:mockito/mockito.dart';

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

class MockExample extends Mock implements Example {}

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; // Add a definition for Example's _i getter.
}

f(Example x) {
  if (x._i != null) {
    int i = x._i; // OK
  }
}

getter 宣告為 late,以符合一般使用 mock 的方式;在不涉及 mock 的情況下,不需要宣告 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 從非 Null 的 int 降級回可為 Null 的 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 區塊的任何時間發生,甚至在 i 為 null 時。

最安全的解決方案是在 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.
  }
}

或者,如果您確定在 i 為 null 時不會發生例外狀況,請使用 ! 算子

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 可能具有同時實作 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 的問題再次發生。

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

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