內容

Null 安全性:常見問題

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

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

#

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

  • 使用者在遷移其程式碼時,會先套用靜態 null 安全性檢查。
  • 當所有程式碼都已遷移且已開啟嚴謹模式時,就會進行完整的 null 安全性檢查。

需要注意的兩個例外情況為

  • ! 營運子在所有模式中對所有使用者都是執行時期 null 檢查。因此,在遷移時,請確保只在 null 流向該位置會發生錯誤的情況下才加入 !,即使呼叫程式碼尚未遷移也是如此。
  • late 關鍵字相關聯的執行時期檢查會套用在所有模式中,對所有使用者都是如此。只有在確定欄位在使用前一定會初始化的情況下,才將欄位標記為 late

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

#

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

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

#

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

使用 Null 安全時,具有非 Null 類型的名稱參數必須具有預設值或標記為新的 required 關鍵字。否則,它是非 Null 的說法沒有意義,因為在未傳遞時,它會預設為 null

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

當從 Null 安全程式碼呼叫 Null 安全程式碼時,未提供 required 參數會導致錯誤。

這對移轉有何意義?在以前沒有 @required 的地方新增 required 時要小心。任何未傳遞新要求參數的呼叫程式都無法再編譯。您可新增預設值或讓參數類型可為 Null。

我該如何移轉應該為 final 但卻不是的非 Null 欄位?

#

部分運算可以移至靜態初始化程式。請執行下列動作,而非

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 安全時,您會發現這也會讓它更難以成為非 Null;如果初始化過晚,則在初始化之前它會為 null,且必須為可為 Null。很幸運地,您有其他選擇

  • 將建構函式轉換為工廠,然後讓它委派給實際建構函式,直接初始化所有欄位。此類私人建構函式的常見名稱只是一個底線:_。然後,欄位可以為 final 且非 Null。此重構可以在移轉至 Null 安全之前執行。
  • 或者,將欄位標記為 late final。這會強制執行它只初始化一次。它必須在讀取之前初始化。

我該如何移轉 built_value 類別?

#

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

dart
@nullable
int get count;

變成

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

未標記為 @nullable 的 getter 不應具有可為 Null 類型,即使移轉工具建議使用。視需要新增 ! 提示,然後重新執行分析。

我該如何遷移可能會傳回 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 來略過警告。

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

#

如果你讓 arg 成為非可為空的,編譯器會將明確的執行時期 null 檢查標記為不必要的比較。

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

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

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

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

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

#

匯入 package:collection 並使用延伸方法 firstWhereOrNull 取代 firstWhere

我應該如何處理有設定項的屬性?

#

與上述的 late final 建議不同,這些屬性無法標記為 final。通常,可設定屬性也沒有初始值,因為預期稍後會設定。

在這種情況下,你有兩個選擇

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

  • 如果你確定在存取屬性之前需要設定,請將其標記為 late

    警告:late 關鍵字會新增執行時期檢查。如果任何使用者在 set 之前呼叫 get,他們將在執行時期收到錯誤。

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

#

Map ([]) 上的 查詢運算子 預設會傳回可為空的類型。沒有辦法向語言表示該值保證存在。

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

dart
return blockTypes[key]!;

如果映射傳回 null,這將會引發例外。如果你想要明確處理這種情況

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 可能包含 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?>。如果你沒有在遷移中明確變更清單類型,類型仍可能因為在啟用 Null 安全性時發生的類型推論變更而變更。

解決方法是將這些清單明確建立為 List<dynamic>

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

#

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

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

在這些情況下,遷移工具無法區分防禦性編碼情況和實際預期空值的情況。因此,工具會告訴你它知道什麼(「看起來這個條件永遠為 false!」),並讓你決定要怎麼做。

我應該了解哪些關於編譯成 JavaScript 和 null 安全性的資訊?

#

空值安全性帶來許多好處,例如減少程式碼大小和改善應用程式效能。當編譯成原生目標(例如 Flutter 和 AOT)時,這些好處會更加明顯。先前在製作網路編譯器時所做的工作已導入類似於空值安全性後來導入的最佳化。這可能會讓產生的收益對製作網路應用程式看起來比對原生目標少。

值得強調的幾點注意事項

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

  • 編譯器會產生這些空值斷言,而不管空值安全性的健全性或最佳化等級。事實上,編譯器在使用 -O3--omit-implicit-checks 時不會移除 !

  • 製作 JavaScript 編譯器可能會移除不必要的空值檢查。這是因為製作網路編譯器在空值安全性之前所做的最佳化會在知道值不為空值時移除這些檢查。

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

  • 你可能會注意到開發 JavaScript 編譯器和 Dart VM 對空值檢查有特殊錯誤訊息,但為了讓應用程式保持精簡,製作 JavaScript 編譯器沒有。

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

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

    例如,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);

資源

#