目錄

空值安全:常見問題

此頁面收集了一些關於空值安全的常見問題,這些問題是根據遷移 Google 內部程式碼的經驗而來的。

對於遷移程式碼的使用者,我應該注意哪些執行階段變更?

#

遷移的大部分影響不會立即影響遷移程式碼的使用者

  • 使用者的靜態空值安全檢查會在他們遷移程式碼時首次套用。
  • 當所有程式碼都遷移完成並開啟健全模式時,才會發生完整的空值安全檢查。

有兩項例外情況需要注意

  • ! 運算子在所有模式下都是執行階段空值檢查,適用於所有使用者。因此,在遷移時,請確保只在 null 流向該位置是錯誤時才加入 !,即使呼叫程式碼尚未遷移也是如此。
  • late 關鍵字相關的執行階段檢查適用於所有模式,適用於所有使用者。只有在確定欄位在被使用前始終會初始化時,才將欄位標記為 late

如果值只在測試中為 null 怎麼辦?

#

如果值只在測試中為 null,則可以透過將其標記為非可空,並讓測試傳遞非空值來改善程式碼。

@required 與新的 required 關鍵字有何比較?

#

@required 注釋標記必須傳遞的具名引數;如果沒有,分析器會報告提示。

使用空值安全時,具有非可空類型的具名引數必須具有預設值,或使用新的 required 關鍵字標記。否則,它沒有理由為非可空,因為在未傳遞時它會預設為 null

當從舊版程式碼呼叫空值安全程式碼時,required 關鍵字的處理方式與 @required 注釋完全相同:未能提供引數將導致分析器提示。

當從空值安全程式碼呼叫空值安全程式碼時,未能提供 required 引數是錯誤。

這對遷移有什麼意義?如果在之前沒有 @required 的地方加入 required,請務必小心。任何未傳遞新要求引數的呼叫者都將無法再編譯。相反地,您可以加入預設值或將引數類型設為可空。

我應該如何遷移應該是 final 但不是的非可空欄位?

#

某些計算可以移至靜態初始化程式。而不是

baddart
// Initialized without values
ListQueue _context;
Float32List _buffer;
dynamic _readObject;

Vec2D(Map<String, dynamic> object) {
  _buffer = Float32List.fromList([0.0, 0.0]);
  _readObject = object['container'];
  _context = ListQueue<dynamic>();
}

您可以這樣做

gooddart
// Initialized with values
final ListQueue _context = ListQueue<dynamic>();
final Float32List _buffer = Float32List.fromList([0.0, 0.0]);
final dynamic _readObject;

Vec2D(Map<String, dynamic> object) : _readObject = object['container'];

但是,如果欄位透過在建構式中進行計算來初始化,則它不能是 final。使用空值安全時,您會發現這也使其更難以成為非可空;如果初始化太晚,則它會是 null,直到初始化為止,並且必須是可空的。幸運的是,您有選項

  • 將建構式變成工廠,然後使其委派給直接初始化所有欄位的實際建構式。此類私有建構式的常用名稱只是底線:_。然後,欄位可以是 final 且非可空。此重構可以在遷移到空值安全之前完成。
  • 或者,將欄位標記為 late final。這會強制執行它只初始化一次。它必須在讀取之前初始化。

我應該如何遷移 built_value 類別?

#

@nullable 注釋的 getter 應該改為具有可空類型;然後移除所有 @nullable 注釋。例如

dart
@nullable
int get count;

變成

dart
int? get count; //  Variable initialized with ?

標記為 @nullable 的 getter 不應具有可空類型,即使遷移工具建議它們也是如此。根據需要加入 ! 提示,然後重新執行分析。

我應該如何遷移可以傳回 null 的工廠?

#

偏好不傳回 null 的工廠。我們看到過程式碼因為輸入無效而本應拋出例外狀況,但卻傳回 null。

而不是

baddart
  factory StreamReader(dynamic data) {
    StreamReader reader;
    if (data is ByteData) {
      reader = BlockReader(data);
    } else if (data is Map) {
      reader = JSONBlockReader(data);
    }
    return reader;
  }

執行

gooddart
  factory StreamReader(dynamic data) {
    if (data is ByteData) {
      // Move the readIndex forward for the binary reader.
      return BlockReader(data);
    } else if (data is Map) {
      return JSONBlockReader(data);
    } else {
      throw ArgumentError('Unexpected type for data');
    }
  }

如果工廠的意圖確實是傳回 null,則可以將其變成靜態方法,使其可以傳回 null

我應該如何遷移現在顯示為不必要的 assert(x != null)

#

當一切都完全遷移時,assert 將會是不必要的,但就目前而言,如果您確實想要保留檢查,則需要的。選項

  • 決定 assert 並非真正必要,然後將其移除。這是在啟用 assert 時的行為變更。
  • 決定可以始終檢查 assert,並將其變成 ArgumentError.checkNotNull。這是在未啟用 assert 時的行為變更。
  • 完全保持行為不變:加入 // ignore: unnecessary_null_comparison 以略過警告。

我應該如何遷移現在顯示為不必要的執行階段空值檢查?

#

如果您將 arg 設定為非可空,則編譯器會將明確的執行階段空值檢查標記為不必要的比較。

dart
if (arg == null) throw ArgumentError(...)`

如果程式是混合版本程式,則必須包含此檢查。直到所有內容都完全遷移,且程式碼切換為以健全空值安全執行,arg 才可能會設定為 null

保留行為的最簡單方法是將檢查變更為 ArgumentError.checkNotNull

相同的適用於某些執行階段類型檢查。如果 arg 具有靜態類型 String,則 if (arg is! String) 實際上是在檢查 arg 是否為 null。它可能看起來像是遷移到空值安全表示 arg 永遠不會是 null,但在不健全的空值安全中,它可能是 null。因此,為了保留行為,應保留空值檢查。

Iterable.firstWhere 方法不再接受 orElse: () => null

#

匯入 package:collection 並使用擴充方法 firstWhereOrNull 而不是 firstWhere

我該如何處理具有 setter 的屬性?

#

與上述的 late final 建議不同,這些屬性不能標記為 final。通常,可設定的屬性也沒有初始值,因為它們預期會在稍後某個時間設定。

在這種情況下,您有兩個選項

  • 將其設定為初始值。通常,省略初始值是錯誤,而不是故意的。

  • 如果您確定必須在存取之前設定屬性,請將其標記為 late

    警告:late 關鍵字會加入執行階段檢查。如果任何使用者在 set 之前呼叫 get,他們會在執行階段收到錯誤。

我該如何表示 Map 的傳回值為非可空?

#

Map 上的查找運算子[])預設會回傳可為 null 的類型。沒有任何方式可以向語言表示該值保證存在。

在這種情況下,您應該使用驚嘆號運算子(!)將該值轉換回 V。

dart
return blockTypes[key]!;

如果 map 回傳 null,則會拋出錯誤。如果您想要明確處理這種情況

dart
var result = blockTypes[key];
if (result != null) return result;
// Handle the null case here, e.g. throw with explanation.

為什麼我的 List/Map 上的泛型類型是可空的?

#

像這樣產生可為 null 的程式碼通常是不好的程式碼風格。

baddart
List<Foo?> fooList; // fooList can contain null values

這表示 fooList 可能包含 null 值。如果您使用長度初始化列表並透過迴圈填入,則可能會發生這種情況。

如果您只是使用相同的值初始化列表,則應該改用 filled 建構函式。

baddart
_jellyCounts = List<int?>(jellyMax + 1);
for (var i = 0; i <= jellyMax; i++) {
  _jellyCounts[i] = 0; // List initialized with the same value
}
gooddart
_jellyCounts = List<int>.filled(jellyMax + 1, 0); // List initialized with filled constructor

如果您是透過索引設定列表的元素,或者您使用不同的值填入列表的每個元素,則應該改用列表字面值語法來建立列表。

baddart
_jellyPoints = List<Vec2D?>(jellyMax + 1);
for (var i = 0; i <= jellyMax; i++) {
  _jellyPoints[i] = Vec2D(); // Each list element is a distinct Vec2D
}
gooddart
_jellyPoints = [
  for (var i = 0; i <= jellyMax; i++)
    Vec2D() // Each list element is a distinct Vec2D
];

若要產生固定長度的列表,請使用 List.generate 建構函式,並將 growable 參數設為 false

dart
_jellyPoints = List.generate(jellyMax, (_) => Vec2D(), growable: false);

預設 List 建構式發生了什麼事?

#

您可能會遇到這個錯誤

The default 'List' constructor isn't available when null safety is enabled. #default_list_constructor

預設的列表建構函式會用 null 填滿列表,這是一個問題。

請將其變更為 List.filled(length, default)

我正在使用 package:ffi,並且在遷移時遇到 Dart_CObject_kUnsupported 的失敗。發生了什麼事?

#

透過 ffi 發送的列表只能是 List<dynamic>,而不是 List<Object>List<Object?>。如果您在遷移中沒有明確變更列表類型,則類型可能仍然會因為啟用空值安全時發生的類型推斷變更而發生變更。

修正方法是明確建立這類列表為 List<dynamic>

為什麼遷移工具會在我的程式碼中加入註解?

#

當遷移工具看到在健全模式下執行時總是為 false 或 true 的條件時,它會新增 /* == false *//* == true */ 註解。類似這些註解可能表示自動遷移不正確,需要人工介入。例如

dart
if (registry.viewFactory(viewDescriptor.id) == null /* == false */)

在這些情況下,遷移工具無法區分防禦性程式碼情況和真正預期 null 值的情況。因此,該工具會告訴您它所知道的(「看起來這個條件永遠會是 false!」),並讓您決定該怎麼做。

關於編譯為 JavaScript 和空值安全,我應該知道什麼?

#

空值安全帶來許多好處,例如減少程式碼大小和改善應用程式效能。當編譯為原生目標(如 Flutter 和 AOT)時,這些好處更為明顯。先前在生產 Web 編譯器上的工作引入了與空值安全後來引入的類似最佳化。這可能會使生產 Web 應用程式的結果增益看起來比原生目標少。

以下是一些值得強調的注意事項

  • 生產 JavaScript 編譯器會產生 ! 空值斷言。當您比較新增空值斷言前後的編譯器輸出時,您可能不會注意到它們。這是因為編譯器已經在非空值安全的程式中產生了空值檢查。

  • 無論空值安全的健全性或最佳化層級為何,編譯器都會產生這些空值斷言。事實上,當使用 -O3--omit-implicit-checks 時,編譯器不會移除 !

  • 生產 JavaScript 編譯器可能會移除不必要的空值檢查。這是因為生產 Web 編譯器在空值安全之前所做的最佳化會在知道該值不是 null 時移除這些檢查。

  • 預設情況下,編譯器會產生參數子類型檢查。這些執行階段檢查可確保共變虛擬呼叫具有適當的引數。編譯器會使用 --omit-implicit-checks 選項略過這些檢查。如果程式碼包含無效的類型,則使用此選項可能會產生具有非預期行為的應用程式。為避免任何意外,請繼續為您的程式碼提供強大的測試涵蓋率。特別是,編譯器會根據輸入應符合類型宣告的事實來最佳化程式碼。如果程式碼提供無效類型的引數,則這些最佳化將會是錯誤的,並且程式可能會執行錯誤。這在之前存在不一致的類型時是如此,而現在在健全的空值安全中,不一致的 null 值也是如此。

  • 您可能會注意到開發 JavaScript 編譯器和 Dart VM 對於空值檢查有特殊的錯誤訊息,但為了保持應用程式的大小,生產 JavaScript 編譯器沒有。

  • 您可能會看到錯誤指出在 null 上找不到 .toString。這不是錯誤。編譯器總是會以這種方式編碼一些空值檢查。也就是說,編譯器會透過不加保護地存取接收器的屬性來簡潔地表示某些空值檢查。因此,它會產生 a.toString,而不是 if (a == null) throwtoString 方法定義在 JavaScript 物件中,是一種快速驗證物件不是 null 的方法。

    如果空值檢查後的第一個動作是當值為 null 時會當機的動作,則編譯器可以移除空值檢查,並讓該動作導致錯誤。

    例如,Dart 表達式 print(a!.foo()); 可以直接轉換為

    js
      P.print(a.foo$0());

    這是因為如果 a 為 null,則呼叫 a.foo$() 會當機。如果編譯器內嵌 foo,它將會保留空值檢查。因此,舉例來說,如果 fooint foo() => 1;,則編譯器可能會產生

    js
      a.toString;
      P.print(1);

    如果內嵌的方法首先存取接收器上的欄位,例如 int foo() => this.x + 1;,則生產編譯器可以移除多餘的 a.toString 空值檢查,如非內嵌呼叫,並產生

    js
      P.print(a.x + 1);

資源

#