跳至主要內容

空值安全:常見問題

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

遷移後的程式碼使用者應注意哪些執行階段變更?

#

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

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

需要注意的兩個例外是

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

如果值僅在測試中為 null,該怎麼辦?

#

如果值僅在測試中為 null,則可以透過將其標記為不可空值並使測試傳遞不可空值來改進程式碼。

@required 與新的 required 關鍵字相比如何?

#

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

在空值安全中,具有不可空值類型的具名引數必須具有預設值或標記新的 required 關鍵字。否則,它作為不可空值就沒有意義了,因為在未傳遞時它會預設為 null

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

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

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

我應該如何遷移應該是 final 但不是 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,並且必須是可空值的。幸運的是,您有選項

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

我應該如何遷移 built_value 類別?

#

標記為 @nullable 的 getter 應該改為具有可空值類型;然後移除所有 @nullable 註解。例如

dart
@nullable
int get count;

變成

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

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

我應該如何遷移可以返回 null 的 factory?

#

偏好不返回空值的 factory。 我們已經看到程式碼本意是因輸入無效而拋出例外,但最終卻返回了空值。

取代

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

如果 factory 的意圖確實是返回空值,那麼您可以將其轉換為靜態方法,使其可以返回 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 上的 查閱運算子 ([]) 預設返回可空值類型。沒有辦法向語言表示該值保證存在。

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

dart
return blockTypes[key]!;

如果 map 返回空值,則會拋出錯誤。如果您想要針對該情況進行明確處理

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

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

#

最終得到像這樣的可空值程式碼通常是一種程式碼異味

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

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

如果您只是使用相同的值初始化列表,則應該改用 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 */)

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

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

#

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

一些值得強調的注意事項

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

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

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

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

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

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

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

    例如,Dart 表達式 print(a!.foo()); 可以直接變成

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

    這是因為如果 a 為空值,則呼叫 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);

資源

#