【Flutter2.0 快速上手指南】 Flutter 空安全深入解析

南方吳彥祖_藍斯發表於2021-10-08

在 Flutter 2.0 中,一項重要的升級就是 Dart 支援 [空安全]。[Alex]為我們貼心地翻譯了多篇關於空安全的文章 :[遷移指南]、[深入理解空安全]等。透過 遷移指南 我也將 fps_monitor 遷移空安全。但在對專案適配後,日常開發中我們該怎麼使用?空安全究竟是什麼?下面我們透過幾個練習來快速上手 Flutter 空安全。


一、空安全解決了什麼問題?

要想弄明白空安全是什麼,我們先要知道空安全幫我們解決了什麼?

先來看個例子

void main() {
  String stringNullException;
  print(stringNullException.length);
}
複製程式碼

在適配空安全之前,這段程式碼在  在編譯階段不會有任何提示。但顯然這是一段有問題的程式碼。在 Debug 模式下會丟擲空異常,螢幕爆紅提示。

I/flutter (31305): When the exception was thrown, this was the stack:
I/flutter (31305): #0      Object.noSuchMethod (dart:core-patch/object_patch.dart:53:5)
複製程式碼

在 release 模式下,這個異常會讓整個螢幕變成灰色。

這是一個典型的例子, stringNullException 在沒有賦值的情況下是空的,但是卻我們呼叫了  .length 方法,導致程式異常。

同樣的程式碼在適配空安全之後,在編譯期便給出了報錯提示,開發者可以及時進行修復。

[圖片上傳失敗...(image-89ce46-1633682979923)]

所以簡單的來說, 空安全在程式碼編輯階段幫助我們提前發現可能出現的空異常問題,但這並不意味著程式不會出現空異常


二、如何使用空安全?

那麼空安全包含哪些內容,我們在日常開發的時候該如何使用?下面我們透過 Null safety codelab 中的幾個練習來進行學習。

1、非空型別和可空型別

在空安全中,所有型別在預設情況下都是非空的。例如,你有一個 String 型別的變數,那麼它應該總是包含一個字串。

如果你想要一個 String 型別的變數接受任何字串或者  null,透過在型別名稱後新增一個問號(?)表示該變數可以為空。例如,一個型別為 String? 可以包含任何字串,也可以為空。

練習 A:非空型別和可空型別

void main() {  int a;
  a = null; // 提示錯誤,因為 int a 表示 a 不能為空
  print('a is $a.');
}
複製程式碼

這段程式碼透過 int 宣告瞭變數 a 是一個非空變數,在執行 a = null 的時候報錯。可以修改為 int? 型別,允許 a 為空:

void main() {  int? a; // 表示允許 a 為空
  a = null; 
  print('a is $a.');
}
複製程式碼

練習 B:泛型的可空型別

void main() {  List<String> aListOfStrings = ['one', 'two', 'three'];  List<String> aNullableListOfStrings = [];  // 報錯提示,因為泛型 String 表示非 null
  List<String> aListOfNullableStrings = ['one', null, 'three']; 
  print('aListOfStrings is $aListOfStrings.');  print('aNullableListOfStrings is $aNullableListOfStrings.');  print('aListOfNullableStrings is $aListOfNullableStrings.');
}
複製程式碼

在這個練習中,因為  aListOfNullableStrings 變數的型別是 List<String> ,表示非空的 String 陣列,但在後面建立過程中卻提供了一個  null 元素,引起報錯。因此可以將  null 改成其他字串,或者在泛型中表示為可空的字串。

void main() {  List<String> aListOfStrings = ['one', 'two', 'three'];  List<String> aNullableListOfStrings = [];  // 陣列元素允許為空,所以不再報錯
  List<String?> aListOfNullableStrings = ['one', null, 'three'];  print('aListOfStrings is $aListOfStrings.');  print('aNullableListOfStrings is $aNullableListOfStrings.');  print('aListOfNullableStrings is $aListOfNullableStrings.');
}
複製程式碼

2、空斷言運算子(!)

如果確定某個  可為空的表示式 非空,可以使用空斷言運算子 ! 使 Dart 將其視為非空。透過新增 ! 在表示式之後,可以將其賦值給一個非空變數。

練習 A:空斷言

/// 這個方法的返回值可能為空int? couldReturnNullButDoesnt() => -3;void main() {  int? couldBeNullButIsnt = 1;
  List<int?> listThatCouldHoldNulls = [2, null, 4];  // couldBeNullButIsnt 變數雖然可為空,但是已經賦予初始值,因此不會報錯
  int a = couldBeNullButIsnt;  // 列表泛型中宣告元素可為空,與 int b 型別不匹配報錯
  int b = listThatCouldHoldNulls.first; // first item in the list
  // 上面宣告這個方法可能返回空,而 int c 表示非空,所以報錯
  int c = couldReturnNullButDoesnt().abs(); // absolute value
  print('a is $a.');
  print('b is $b.');
  print('c is $c.');
}
複製程式碼

在這個練習中,方法  couldReturnNullButDoesnt 和陣列  listThatCouldHoldNulls 都透過可空型別進行宣告,但是後面的變數 b 和 c,都是透過非空型別來宣告,因此報錯。可以在表示式最後加上 ! 表示操作非空( 你必須確認這個表示式是一定不會為空,否則仍然可能引起空指標異常)修改如下:

int? couldReturnNullButDoesnt() => -3;void main() {  int? couldBeNullButIsnt = 1;
  List<int?> listThatCouldHoldNulls = [2, null, 4];  int a = couldBeNullButIsnt;  // 新增 ! 斷言 表示非空,賦值成功
  int b = listThatCouldHoldNulls.first!; // first item in the list
  int c = couldReturnNullButDoesnt()!.abs(); // absolute value
  print('a is $a.');
  print('b is $b.');
  print('c is $c.');
}
複製程式碼

3、型別提升

Dart 的  流程分析中已經擴充套件到考慮零值性。 不可能為空的可空變數會被視為非空變數,這種行為稱為型別提升

bool isEmptyList(Object object) {  if (object is! List) return false;  // 在空安全之前會報錯,因為 Object 物件並不包含 isEmpty 方法
  // 在空安全後不報錯,因為流程分析會根據上面的判斷語句將 object 變數提升為 List 型別。
  return object.isEmpty; 
}
複製程式碼

這段程式碼在空安全之前會報錯,因為 object 變數是 Object 型別,並不包含 isEmpty 方法。

在空安全後不會報錯,因為流程分析會根據上面的判斷語句將 object 變數提升為 List 型別。

練習 A:明確地賦值

void main() {
  String? text;  //if (DateTime.now().hour < 12) {  //  text = "It's morning! Let's make aloo paratha!";  //} else {  //  text = "It's afternoon! Let's make biryani!";  //}
  print(text);  // 報錯提示,text 變數可能為空
  print(text.length);
}
複製程式碼

這段程式碼中我們使用 String? 宣告瞭一個可空的變數 text,在後面直接使用了 text.length。Dart 會認為這是不安全的,因此報錯提示。

但當我們去掉上面註釋的程式碼後,將不會在報錯。因為 Dart 對 text 賦值的地方判斷後,認為 text 不會為空,將 text 提升為非空型別(String),不再報錯。

練習 B:空檢查

int getLength(String? str) {  // 此處報錯,因為 str 可能為空
  return str.length;
}void main() {
  print(getLength('This is a string!'));
}
複製程式碼

這個例子中,因為 str 可能為空,所以使用 str.length 會提示錯誤,透過型別提升我們可以這樣修改:

int getLength(String? str) {  // 判斷 str 為空的場景 str 提升為非空型別
  if (str == null) return 0;  return str.length;
}void main() {
  print(getLength('This is a string!'));
}
複製程式碼

提前判斷 str 為空的場景,這樣後面 str 的型別由 String?(可空)提升為 String(非空),不再報錯。

3、late 關鍵字

有時變數(例如:類中的欄位或頂級變數)應該是非空的,但不能立即給它們賦值。對於這種情況,使用 late 關鍵字。

當你把 late 放在變數宣告的前面時,會告訴 Dart 以下資訊:

  • 先不要給變數賦值。
  • 稍後將為它賦值
  • 你會在使用前對這個變數賦值。
  • 如果在給變數賦值之前讀取該變數,則會丟擲一個錯誤。

練習 A:使用 late

class Meal {  // description 變數沒有直接或者在建構函式中賦予初始值,報錯
  String description;  void setDescription(String str) {
    description = str;
  }
}void main() {  final myMeal = Meal();
  myMeal.setDescription('Feijoada!');
  print(myMeal.description);
}
複製程式碼

這個例子中,Meal 類包含一個非空變數 description,但該變數卻沒有直接或者在建構函式中賦予初始值,因此報錯。這種情況下,我們可以使用  late 關鍵字 表示這個變數是延遲宣告:

class Meal {  // late 宣告不在報錯
  late String description;  void setDescription(String str) {
    description = str;
  }
}void main() {  final myMeal = Meal();
  myMeal.setDescription('Feijoada!');
  print(myMeal.description);
}
複製程式碼

練習 B:迴圈引用下使用 late

class Team {  // 非空變數沒有初始值,報錯
  final Coach coach;
}class Coach {  // 非空變數沒有初始值,報錯
  final Team team;
}void main() {  final myTeam = Team();  final myCoach = Coach();
  myTeam.coach = myCoach;
  myCoach.team = myTeam;
  print('All done!');
}
複製程式碼

透過新增 late 關鍵字解決報錯。注意,我們不需要刪除 final。 late final 宣告的變數表示: 只需設定它們的值一次,然後它們就成為只讀變數

class Team {
  late final Coach coach;
}class Coach {
  late final Team team;
}void main() {  final myTeam = Team();  final myCoach = Coach();
  myTeam.coach = myCoach;
  myCoach.team = myTeam;
  print('All done!');
}
複製程式碼

練習 C:late 關鍵字和懶載入

int _computeValue() {  print('In _computeValue...');  return 3;
}class CachedValueProvider {  final _cache = _computeValue();
  int get value => _cache;
}
void main() {  print('Calling constructor...');  var provider = CachedValueProvider();  print('Getting value...');  print('The value is ${provider.value}!');
}
複製程式碼

這個練習並不會報錯,不過可以看看執行這段程式碼的輸出結果:

Calling constructor...
In _computeValue...
Getting value...
The value is 3!
複製程式碼

在列印完第一句  Calling constructor... 之後,生成  CachedValueProvider() 物件。生成過程會初始化它的變數  final _cache = _computeValue() 所以列印第二句話  In _computeValue...,再列印後續的語句。

當我們對 _cache 變數新增 late 關鍵字後,結果又如何?

int _computeValue() {  print('In _computeValue...');  return 3;
}class CachedValueProvider {  // late 關鍵字,該變數不會在構造的時候初始化
  late final _cache = _computeValue();
  int get value => _cache;
}
void main() {  print('Calling constructor...');  var provider = CachedValueProvider();  print('Getting value...');  print('The value is ${provider.value}!');
}
複製程式碼

日誌如下:

Calling constructor...
Getting value...
In _computeValue...
The value is 3!
複製程式碼

日誌中 In _computeValue... 的執行被延後了,其實就是 _cache 變數沒有在構造的時候初始化,而是延遲到了使用的時候。


四、空安全並不意味沒有空異常

這幾個練習,也更加的反應了安全的作用: 空安全在程式碼編輯階段幫助我們提前發現可能出現的空異常問題。但要注意, 這並不意味著不存在空異常。例如下面的例子

void main() {
  String? text;
  print(text);  // 不會報錯,因為使用 ! 斷言 表示 text 變數不可能為空
  print(text!.length);
}
複製程式碼

因為  text!.length 表示變數 text 不可能為空。但實際上 text 可能因為各種原因(例如,json 解析為 null)為空,導致程式異常。

上面 late 關鍵字的場景同樣也會存在:

class Meal {  // late 宣告編輯階段將不會報錯
  late String description;  void setDescription(String str) {
    description = str;
  }
}void main() {  final myMeal = Meal();  // 先去讀取這個未初始化變數,導致異常
  print(myMeal.description);
  myMeal.setDescription('Feijoada!');
}
複製程式碼

我們在對  description 賦值之前提前讀取,同樣會導致程式異常。

所以還是那句話: 空安全只是在程式碼編輯階段幫助我們提前發現可能出現的空異常問題,但這並不意味著程式不會出現空異常。開發者任需要對程式碼進行完善的邊界判斷,確保程式的健壯執行!

更多Android技術分享可以關注@我,也可以加入Android進階學習群(QQ群):345659112,一起學習交流,裡面整理收集了最詳細的Flutter進階與最佳化指南。

作者:Nayuta
連結:
來源:稀土掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2794965/,如需轉載,請註明出處,否則將追究法律責任。

相關文章