空安全:常見問題
- 對於已遷移程式碼的使用者,我應該注意哪些執行時變更?
- 如果一個值只在測試中為 null 怎麼辦?
- @required 與新的 required 關鍵字有何區別?
- 如何遷移應為 final 但不是 final 的非可空欄位?
- 如何遷移一個 built_value 類?
- 如何遷移一個可能返回 null 的工廠?
- 如何遷移現在顯示為不必要的 assert(x != null)?
- 如何遷移現在顯示為不必要的執行時 null 檢查?
- Iterable.firstWhere 方法不再接受 orElse: () => null。
- 如何處理帶有 setter 的屬性?
- 如何表示 Map 的返回值是非可空的?
- 為什麼我的 List/Map 上的泛型型別是可空的?
- 預設 List 建構函式發生了什麼?
- 我正在使用 package:ffi,遷移時遇到 Dart_CObject_kUnsupported 失敗。發生了什麼?
- 為什麼遷移工具會向我的程式碼添加註釋?
- 關於編譯到 JavaScript 和空安全,我應該瞭解什麼?
- 資源
本頁面收集了一些我們從遷移 Google 內部程式碼的經驗中聽到的關於空安全的常見問題。
對於已遷移程式碼的使用者,我應該注意哪些執行時變更?
#遷移的大部分影響不會立即影響已遷移程式碼的使用者
- 使用者的靜態空安全檢查在他們遷移程式碼時首次生效。
- 當所有程式碼都已遷移並且健全模式開啟時,會進行完整的空安全檢查。
需要注意的兩個例外是
- 在所有模式下,對於所有使用者,
!運算子都是一個執行時 null 檢查。因此,在遷移時,請確保僅在null流入該位置會出錯的地方新增!,即使呼叫程式碼尚未遷移。 - 與
late關鍵字關聯的執行時檢查在所有模式下對所有使用者都適用。僅當您確定欄位在使用前已始終初始化時,才將其標記為late。
如果一個值只在 null 在測試中為 null 怎麼辦?
#如果一個值只在測試中為 null,可以透過將其標記為非可空並在測試中傳遞非 null 值來改進程式碼。
@required 與新的 required 關鍵字有何區別?
#@required 註解標記必須傳遞的命名引數;否則,分析器會報告提示。
使用空安全,具有非可空型別的命名引數必須具有預設值或使用新的 required 關鍵字標記。否則,它就沒有理由是非可空的,因為它在未傳遞時將預設為 null。
當從舊版程式碼呼叫空安全程式碼時,required 關鍵字的處理方式與 @required 註解完全相同:未能提供引數將導致分析器提示。
當從空安全程式碼呼叫空安全程式碼時,未能提供 required 引數是一個錯誤。
這對遷移意味著什麼?如果之前沒有 @required 而現在新增 required,請務必小心。任何未傳遞新要求的引數的呼叫者將不再編譯。相反,您可以新增一個預設值或使引數型別可空。
如何遷移應為 final 但不是 final 的非可空欄位?
#某些計算可以移到靜態初始化器中。而不是
// 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>();
}你可以這樣做
// 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 註解。例如
@nullable
int get count;變為
int? get count; // Variable initialized with ?未標記為 @nullable 的 getter 不應具有可空型別,即使遷移工具建議這樣做。根據需要新增 ! 提示,然後重新執行分析。
如何遷移一個可能返回 null 的工廠?
#優先選擇不返回 null 的工廠。 我們見過一些程式碼,本意是因無效輸入丟擲異常,結果卻返回了 null。
而不是
factory StreamReader(dynamic data) {
StreamReader reader;
if (data is ByteData) {
reader = BlockReader(data);
} else if (data is Map) {
reader = JSONBlockReader(data);
}
return reader;
}這樣做
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 檢查標記為不必要的比較。
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
return blockTypes[key]!;如果 map 返回 null,這將丟擲。如果您想對這種情況進行顯式處理
var result = blockTypes[key];
if (result != null) return result;
// Handle the null case here, e.g. throw with explanation.為什麼我的 List/Map 上的泛型型別是可空的?
#最終出現這樣的可空程式碼通常是一種程式碼異味
List<Foo?> fooList; // fooList can contain null values這意味著 fooList 可能包含 null 值。如果您使用長度初始化列表並透過迴圈填充它,則可能會發生這種情況。
如果您只是使用相同的值初始化列表,則應改為使用 filled 建構函式。
_jellyCounts = List<int?>(jellyMax + 1);
for (var i = 0; i <= jellyMax; i++) {
_jellyCounts[i] = 0; // List initialized with the same value
}_jellyCounts = List<int>.filled(jellyMax + 1, 0); // List initialized with filled constructor如果您正在透過索引設定列表元素,或者您正在用不同的值填充列表的每個元素,則應改為使用列表字面量語法構建列表。
_jellyPoints = List<Vec2D?>(jellyMax + 1);
for (var i = 0; i <= jellyMax; i++) {
_jellyPoints[i] = Vec2D(); // Each list element is a distinct Vec2D
}_jellyPoints = [
for (var i = 0; i <= jellyMax; i++)
Vec2D() // Each list element is a distinct Vec2D
];要生成固定長度的列表,請使用將 growable 引數設定為 false 的 List.generate 建構函式。
_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 */ 註釋。此類註釋可能表明自動遷移不正確,需要人工干預。例如
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.toString。toString方法在 JavaScript Object 中定義,是驗證物件是否非 null 的快速方法。如果在 null 檢查之後的第一個操作是當值為 null 時會崩潰的操作,編譯器可以移除 null 檢查,讓該操作導致錯誤。
例如,Dart 表示式
print(a!.foo());可以直接轉換為jsP.print(a.foo$0());這是因為如果
a為 null,呼叫a.foo$()將會崩潰。如果編譯器內聯foo,它將保留 null 檢查。因此,例如,如果foo是int foo() => 1;,編譯器可能會生成jsa.toString; P.print(1);如果內聯方法首先訪問了接收器上的一個欄位,例如
int foo() => this.x + 1;,那麼生產編譯器可以移除冗餘的a.toStringnull 檢查(作為非內聯呼叫),並生成jsP.print(a.x + 1);