Dart Sound Null Safety 深入分析

aboutlan發表於2021-04-03

Dart Sound Null Safety 深入分析

此文章是學習Understanding null safety的簡單總結

型別系統裡面的可空特性

null 是非常有用的,可代表為一個不存在的值,但如果不注意就會引起異常。

null 本質是 Null 類的例項, 可視為其他任何型別的子型別,換句話說 int num = null; 實際是多型的一種表現形式。空指標呼叫方法/屬性錯誤的原因(即NoSuchMethodError 異常)即是來自於呼叫 null 裡面不存在的方法或屬性。 image.png

型別系統裡的可空與不可空

只要更改 Null 的型別層級結構,沒有型別可以直接在為 null, 該變數即為 non-nullable, 可解決空指標問題。

image.png

nullable 型別更像是一個基本資料型別和 Null 的聯合體(例如: String?),意味著 Null 會是任何 nullable 型別的子型別。 image.png

結合下面例子, info['familyName'] 取出來將會是一個 null 值, 如果直接轉換成 String 那麼就會拋 type 'Null' is not a subtype of type 'String' in type cast 結合第一張圖可以知曉原因,null 不是 non-nullable 型別的子型別。 但如果轉換為 String? 就能夠成功執行,結合第二張圖,因為 null 是 String? (nullable 型別的子型別)的子型別。

  Map<String, dynamic> info = {'name': 'laozhang'};
  // String familyName = info['familyName'] as String; // error
  String? familyName = info['familyName'] as String?; // ok
  print(familyName);
複製程式碼

所有的型別就好像被分割成了兩部分,non-nullable 型別側你可以隨意的進行訪問方法/屬性,不用擔心會產生空指標問題。non-nullable 側的變數可以賦值給 nullable 側的變數,因為這是安全的,反之則不行。 image.png

頂部與底部型別

在 null-safey 之前 Object 是型別層級樹中最頂部的型別,而 Null 則是最底部的型別。 在一個 non-nullable 型別中, Object? 將是其頂部型別而 Never 是其底部型別。 也就是說在 null-safey 中如果要接收一個任意型別的值,使用 Object? 而不是 Object 型別。表示一個底部型別使用 Never 而不是 Nullimage.png

特性與分析進行優化與改進

以下優化即是讓靜態分析變得更聰明瞭更敏銳了。

返回值

在 null-safety 在函式中如果沒有使用 return 返回一個值只會報警告,預設會返回 null。 在 null-safety 中對一個返回值是 non-nullable 型別不允許這麼做,必須要有明確的返回值。

變數初始化

以下規則僅限於 non-nullable 型別變數初始化規則如下,原則即是要用之前必須初始化過:

  1. 全域性變數和靜態變數宣告必須要初始化
// Using null safety:
int topLevel = 0;
class SomeClass {
 static int staticField = 0;
}
複製程式碼
  1. 例項變數直接宣告時初始化或通過構造器來初始化(或使用 late)
class SomeClass {
int atDeclaration = 0;
int initializingFormal;
int initializationList;

SomeClass(this.initializingFormal)
    : initializationList = 0;
}
複製程式碼
  1. 區域性變數什麼時候賦值都可以,但在使用前必須已經賦過值
// Using null safety:
int tracingFibonacci(int n) {
int result;
if (n < 2) {
  result = n;
} else {
  result = tracingFibonacci(n - 2) + tracingFibonacci(n - 1);
}

print(result);
return result;
}
複製程式碼
  1. 命名引數必須有預設值或者是 request 關鍵詞修飾。

型別提升

在 null-safety 中解決了對型別提升分析的優化, 如下程式碼也可以正常執行,Object 例項在 is 判斷後會被提升為 List 例項

bool isEmptyList(Object object) {
  if (object is! List) return false;
  return object.isEmpty; // <-- Error!
}
複製程式碼

Never

never 可用於表示中斷、拋異常。 可作為型別來使用

Never wrongType(String type, Object value) {
  throw ArgumentError('Expected $type, but was ${value.runtimeType}.');
}
複製程式碼

賦值

對於以 final 修飾的變數賦值分析變得更加靈活、更加聰明瞭,以下程式碼在 null-safety 不再會報錯。

// Using null safety:
int tracingFibonacci(int n) {
  final int result;
  if (n < 2) {
    result = n;
  } else {
    result = tracingFibonacci(n - 2) + tracingFibonacci(n - 1);
  }

  print(result);
  return result;
}
複製程式碼

null 檢查進行型別提升

對 nullable 變數進行 == null 或 != null 等等表示式判斷, Dart 會將變數型別提升到 non-nullable 型別, 在對 arguments != null 表示式判斷後 List<String>? 提升了為 List<String>

需要注意的是型別提升對類中的欄位是沒有效果的。因為欄位使用太靈活,靜態分析無法判斷哪裡有使用哪裡有檢查

// Using null safety:
String makeCommand(String executable, [List<String>? arguments]) {
  var result = executable;
  if (arguments != null) {
    result += ' ' + arguments.join(' ');
  }
}
// or
// Using null safety:
String makeCommand(String executable, [List<String>? arguments]) {
  var result = executable;
  if (arguments == null) return result;
  return result + ' ' + arguments.join(' ');
}
複製程式碼

多餘程式碼警告(Unnecessary code warnings)

對一個 non-nullable 型別進行 null 檢查,如: ?.、 == null 、 != null 等等靜態分析會丟擲警告或錯誤。

// Using null safety:
String checkList(List? list) {
  if (list == null) return 'No list';
  if (list?.isEmpty) { // list?.isEmpty Unnecessary code
    return 'Empty list';
  }
  return 'Got something';
}
複製程式碼

Nullable 型別處理

空敏感操作符(null-aware)

在 null-safty 之前,由於不知道呼叫鏈中哪個節點可能會出現 null, 所有每個屬性/方法呼叫全部都用可空 ?.

String? notAString = null;
print(notAString?.length?.isEven);
複製程式碼

在 null-safty 中, 呼叫者一旦為 null,那麼鏈中剩下的方法就會被跳過,不會執行,即短路。

thing?.doohickey.gizmo
複製程式碼

類似空敏感操作符還有: ?.. ?[]

// Null-aware cascade:
receiver?..method();

// Null-aware index operator:
receiver?[index];
複製程式碼

斷言操作符(Null assertion operator)

當一個 nullable 變數可以確認它不會為 null 時,可以通過 as 轉換或者 ! 來斷言其不會為 null。如果轉換失敗或斷言失敗則會拋異常,反之即會轉換成 non-nullable 型別。

// Using null safety:
String toString() {
  if (code == 200) return 'OK';
  return 'ERROR $code ${(error as String).toUpperCase()}';
}

// Using null safety:
String toString() {
  if (code == 200) return 'OK';
  return 'ERROR $code ${error!.toUpperCase()}';
}
複製程式碼

Late 變數

類的 non-nullable 屬性或欄位在使用前必須要被初始化(在構造器初始化列表或是給預設值都可以),但也可以使用修飾符 late,late 作用將對變數約束從編譯時延遲到執行時。但需要注意的是,沒有賦值就在執行時使用同樣會拋異常。

懶載入

late 還有個作用就是可以對變數懶載入, _temperature 變數不會在構造例項的時候就直接建立而是會延遲到第一次訪問這個。如果這個操作_temperature 變數時候。 對於一些消耗大量資源的操作,可以在有需要的時候再進行。

預設上由於例項還沒有構造完畢,所以不允許初始化值是例項的方法/屬性。但由於是懶載入,這個初始化值現在就可以訪問當前例項的方法/屬性,比如在 _readThermometer() 方法就是因為 late 關鍵字所以可以訪問。

// Using null safety:
class Weather {
  late int _temperature = _readThermometer();
}
複製程式碼

Late final 結合

含義即是: 對於 non-nullable 變數不需要在宣告時或構造器進行初始化且必須賦值有且只有一次。

Required 修飾符

防止 non-nullable 命名引數為 null,型別檢查要求命名引數必須使用 required 修飾或者給引數預設值。

nullable 欄位的處理

上文講過型別提升對類中的欄位是沒有效果的如下程式碼,編譯器會報錯:

class Coffee {
  String? _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }

  void checkTemp() {
    if (_temperature != null) {
      print('Ready to serve ' + _temperature + '!');
    }
  }

  String serve() => _temperature! + ' coffee';
}
複製程式碼

要處理有幾種方案,一種是直接對 _temperature 加斷言操作符 !。另外就是將變數先拷貝成區域性變數再做型別提升,如果區域性變數改了值要記得改賦值回欄位。

// Using null safety:
void checkTemp() {
  var temperature = _temperature;
  if (temperature != null) {
    print('Ready to serve ' + temperature + '!');
  }
}
複製程式碼

泛型的可空性

T 可以是 nullable 型別也可以是 non-nullable 型別。

// Using null safety:
class Box<T> {
  final T object;
  Box(this.object);
}

main() {
  Box<String>('a string');
  Box<int?>(null);
}
複製程式碼

相關文章