跳到主要內容

空安全:常見問題

本頁面收集了一些我們從遷移 Google 內部程式碼的經驗中聽到的關於空安全的常見問題。

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

#

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

  • 使用者的靜態空安全檢查在他們遷移程式碼時首次生效。
  • 當所有程式碼都已遷移並且健全模式開啟時,會進行完整的空安全檢查。

需要注意的兩個例外是

  • 在所有模式下,對於所有使用者,! 運算子都是一個執行時 null 檢查。因此,在遷移時,請確保僅在 null 流入該位置會出錯的地方新增 !,即使呼叫程式碼尚未遷移。
  • late 關鍵字關聯的執行時檢查在所有模式下對所有使用者都適用。僅當您確定欄位在使用前已始終初始化時,才將其標記為 late

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

#

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

@required 與新的 required 關鍵字有何區別?

#

@required 註解標記必須傳遞的命名引數;否則,分析器會報告提示。

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

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

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

這對遷移意味著什麼?如果之前沒有 @required 而現在新增 required,請務必小心。任何未傳遞新要求的引數的呼叫者將不再編譯。相反,您可以新增一個預設值或使引數型別可空。

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

#

某些計算可以移到靜態初始化器中。而不是

dart
// 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>();
}

你可以這樣做

dart
// 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。

而不是

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

這樣做

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

#

當所有內容都完全遷移後,斷言將變得不必要,但目前如果您確實想保留檢查,它必需的。選項

  • 決定斷言確實不必要,並將其刪除。這會在斷言啟用時改變行為。
  • 決定斷言可以始終檢查,並將其轉換為 ArgumentError.checkNotNull。這會在斷言未啟用時改變行為。
  • 保持原樣:新增 // ignore: unnecessary_null_comparison 以繞過警告。

如何遷移現在顯示為不必要的執行時 null 檢查?

#

如果您將 arg 設為非可空,編譯器會將顯式執行時 null 檢查標記為不必要的比較。

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

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

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

這同樣適用於一些執行時型別檢查。如果 arg 的靜態型別是 String,那麼 if (arg is! String) 實際上是在檢查 arg 是否為 null。看起來遷移到空安全意味著 arg 永遠不會是 null,但在不健全的空安全中它可能是 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 上的泛型型別是可空的?

#

最終出現這樣的可空程式碼通常是一種程式碼異味

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

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

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

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

如果您正在透過索引設定列表元素,或者您正在用不同的值填充列表的每個元素,則應改為使用列表字面量語法構建列表。

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

要生成固定長度的列表,請使用將 growable 引數設定為 falseList.generate 建構函式。

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 值的情況。因此,該工具會告訴您它所知道的(“這個條件看起來總是假的!”),並讓您決定如何處理。

關於編譯到 JavaScript 和空安全,我應該瞭解什麼?

#

空安全帶來了許多好處,例如減小程式碼大小和提高應用效能。當編譯為 Flutter 和 AOT 等原生目標時,這些好處會更加明顯。之前在生產 Web 編譯器上的工作引入了與空安全後來引入的類似最佳化。這可能使生產 Web 應用的收益看起來不如其原生目標。

以下幾點值得強調

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

  • 編譯器生成這些非 null 斷言,無論空安全的健全性或最佳化級別如何。實際上,在使用 -O3--omit-implicit-checks 時,編譯器不會移除 !

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

  • 預設情況下,編譯器會生成引數子型別檢查。這些執行時檢查確保協變虛擬函式呼叫具有適當的引數。編譯器會使用 --omit-implicit-checks 選項跳過這些檢查。如果程式碼包含無效型別,使用此選項可能會生成具有意外行為的應用。為避免任何意外,請繼續為您的程式碼提供強大的測試覆蓋。特別是,編譯器會根據輸入應符合型別宣告的事實來最佳化程式碼。如果程式碼提供了無效型別的引數,則這些最佳化將是錯誤的,並且程式可能會出現異常行為。這在以前對於不一致的型別是如此,現在對於健全空安全中的不一致可空性也是如此。

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

  • 您可能會看到錯誤指示在 null 上找不到 .toString。這不是一個 bug。編譯器一直以這種方式編碼一些 null 檢查。也就是說,編譯器透過對接收者的屬性進行無保護訪問來緊湊地表示一些 null 檢查。因此,它不是生成 if (a == null) throw,而是生成 a.toStringtoString 方法在 JavaScript Object 中定義,是驗證物件是否非 null 的快速方法。

    如果在 null 檢查之後的第一個操作是當值為 null 時會崩潰的操作,編譯器可以移除 null 檢查,讓該操作導致錯誤。

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

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

    這是因為如果 a 為 null,呼叫 a.foo$() 將會崩潰。如果編譯器內聯 foo,它將保留 null 檢查。因此,例如,如果 fooint foo() => 1;,編譯器可能會生成

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

    如果內聯方法首先訪問了接收器上的一個欄位,例如 int foo() => this.x + 1;,那麼生產編譯器可以移除冗餘的 a.toString null 檢查(作為非內聯呼叫),並生成

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

資源

#