前言
Flutter 2.0的使用已經有一段時間了,想在此分享一些關於空安全的使用經驗和個人理解。
Dart空安全
空安全是什麼?
在空安全下,執行時的NullPointer Exception
錯誤被提前到了開發階段。
即:
void main() {
// 在空安全下, 開發階段就會報錯,而非執行時
String name;
debugPrint(name);
}
複製程式碼
型別上也有了變化,這裡貼兩張官方的型別關係圖深入理解空安全 :
非空安全時代:
空安全時代
可以看到,Null
這個型別變成了一個單獨的類,而非所有類的子類,換言之:
void main() {
String a = null;
}
非空安全下:
因為Null是所有型別的子類,基於多型性的原理,這種書寫方式是正確的。
在空安全下:
Null獨立了出去,那麼當你再像上面那樣書寫時,就會報錯了,因為這本質上發生了型別轉換的錯誤.
複製程式碼
空安全有什麼?
新增了關鍵字,並對原有關鍵字的含義做了擴充 :
? -> 可空, 如: int a?;
! -> 非空 如: int b = a!;
late -> 延遲初始化 如: late int a;
required -> 可選引數的不可空 如: {required int a}
複製程式碼
而含義的物件就是開發工具了,下面逐一對它們進行介紹。
關鍵字: ?
我們在開發過程中,相當多的引數變數並不一定為必傳,因此我們可以用 ?
來標註其為可空。
///通過問號,我們可以告知編輯器(及使用者),style是可空的(有點像可選引數)
Widget buildTextWidget(String text, TextStyle? style) {
return Text(text, style: style);
}
呼叫時:
void main() {
buildTextWidget('hello world !',null);
buildTextWidget('hello world !',customStyle);
}
複製程式碼
當然,上面的方法還可以改為使用可選引數:
///命名引數
Widget buildTextWidget(String text, {TextStyle? style}) {
return Text(text, style: style);
}
///位置引數
Widget buildTextWidget(String text, [TextStyle? style]) {
return Text(text, style: style);
}
複製程式碼
我個人很喜歡用命名引數,因為在使用時會顯示對應引數名,這樣使用者可以大致知道他傳入的變數用來做什麼。
說到這,我們就來介紹下 required
。
關鍵字: required
當我們希望通過命名引數來提高方法的呼叫便捷性和可讀性時,會遇到一個問題,即:可選引數是可以不傳的。
如果引數是可空的,那麼還好,如果遇到不可空的就麻煩了:
/// 此時編輯器就會報錯,因為 i 變數可能為空
Demon invoke(Target target, {Invoker invoker, String? way, String? material}) {
...
}
複製程式碼
當遇到上面的情況時,而我們又確實需要使用這個變數,那麼有3種解決辦法:
1、 不使用可選引數
2、 設定預設值
3、 使用required 告知編輯器此引數不能為空 (無法使用-方法 2)
複製程式碼
修改後:
///此時可以通過編譯
Demon invoke(Target target, {required Invoker invoker, String? way, String? material}) {
...
}
/// 呼叫時,如果不傳 引數 invoker, 或者傳入空,那麼編輯器將會報錯
invoke(a); //錯誤
invoke(a, invoker: null); // 錯誤
複製程式碼
在實際開發中,我們可能需要通過某個方法來初始化一個成員變數,如下類:
class WarLock {
//報錯
Demon cardDemon;
Demon invoke(Target target, {required Invoker invoker, String? way, String? material}) {
...
}
}
複製程式碼
當我們寫完後,會發現變數 cardDemon
報錯,編輯器要求你必須對它初始化。我們可以直接加個 ?
告訴編輯器此值可空。
Demon? cardDemon;
複製程式碼
那麼,當我們隨後使用它的時候,編輯器就會要求我們進行判空處理(否則無法通過編譯),如果使用位置非常多那就蛋疼了。此外,對於關鍵變數,這種方法會對後期維護或者接手的小夥伴產生誤導。
某些情境下,成員變數需要方法來(如:建構函式內依賴入參)初始化。
同時可以確定,後續的使用總是在其初始化之後。那麼此處使用 '?' 將是不嚴謹的。
複製程式碼
因此,late
就應運而生了。
關鍵字: late
它可以告訴編輯器:這個非空變數,我稍後會初始化。
class WarLock {
late Demon cardDemon;
//如果我們規定此變數不可變,那麼我們還可以這樣寫
// late final Demon cardDemon;
Demon invoke(Target target, {required Invoker invoker, String? way, String? material}) {
...
}
}
複製程式碼
此時,我們再使用這個變數時,編輯器將會不會報錯,同時也不需要判空。
而這裡也就引申出一個問題: 空安全並不意味著不會出現空指標的異常。
class Test{
late int a;
void showMsg() {
debugPrint('$a'); //編輯器並不會報錯,類似的問題後面還會出現
}
}
複製程式碼
接下來我們看最後一個關鍵字。
關鍵字: !
當我們在使用可空的變數時,如果在一個方法塊(如:if)內進行了判空,並做了空退出,同方法塊內的後續使用依然會出現編輯器的報錯 :
class TestEntity{
int? count;
...
void doStuff() {
count = 0;
if(count == null) return;
//報錯:A value of type 'int?' can't be assigned to a variable of type 'int'.
int b = count;
}
}
複製程式碼
這時,我們就需要使用 !
來告訴編輯器,我確認這個值不會為空。
int b = count!; //不會報錯
複製程式碼
另一方面,從Flutter的單執行緒
和Isolate特性
的角度來看,執行到 int b = count;
時是不可能為空的,為什麼還會報錯呢?
因為 空安全 是語言機制,而非Flutter機制,在方法內使用成員變數時,並不能確保是否有其他執行緒對其進行了置空操作。(Flutter是支援多Isolate執行的)
其次,不考慮上面的情況,我們依然可以寫出 空異常 的程式碼 :
class TestEntity{
int? count;
...
///呼叫一個非同步方法
void doStuff() async {
...
count = 0;
trigerNullException();
...
}
Future trigerNullException() async {
if(count == null) return ;
//程式碼會等待1秒後,繼續執行
await Future.delayed(const Duration(seconds: 1), () {
count = null;
});
// 此時將會丟擲:
// Unhandled Exception: Null check operator used on a null value
int b = count!;
}
}
複製程式碼
Tip: 實際上這裡的非同步排程還是藉助了其他執行緒(Engine層)。
誠然,上方程式碼Future.delayed()
中的錯誤一目瞭然,但在實際開發過程中, 取而代之的可能是一個有著複雜呼叫鏈的方法(甚至多人負責的),那麼我們就很難把控執行到int b = c!;
時, count 不為空了。
空安全帶來了什麼?
在我看來,空安全為開發設計規範增加了 "法律效力"。
非空時,我們可以隨意定義變數、引數和方法,
class A{
int a;
String txt;
double height;
A(this.a, this.txt,{this.height});
String generateContent({String rawTxt}) {
return '$rawTxt : $height';
}
}
複製程式碼
對於使用者,我們只能通過引數型別必選引數 或 可選引數
來判斷是否為可空 (前提是雙方遵守這個約定俗成的規矩)
,而其核心與否就只能期盼註釋的闡明瞭。
方法的使用,更是隻能採取保守策略,對返回結果增加一些安全措施。再複雜一些的,就只能去找對應開發同學了~
空安全下,上述方式的編寫風格,將會在開發階段被標紅,設計規範被強制化。
如:入參的要求和個數,設計時需要更為嚴謹和收斂,因為每一個變數我們都需要處理,否則將無法通過編譯。
複製程式碼
我們在定義類、引數變數及方法時可以(也需要)通過 空安全關鍵字 : ? 、 ! 、 required 、late
來告知使用者,哪些變數是核心(不僅限於非空)、而哪些不是;這個方法肯定能返回結果(非空),那個方法可能返回結果 ,
String methodOne() {...}
String? methodTwo() {...}
複製程式碼
而 開發工具(空安全) 則為這個規範得以被遵守提供了有力的保障(無法編譯)。
這也算是被動提高了開發和使用規範。
一言以蔽之:分鍋更為明確~ 哈哈哈
複製程式碼
結語
最後,謝謝大家的閱讀,如有錯誤歡迎指出。
其他Flutter相關文章