關於Flutter空安全的一些使用經驗和理解

吉哈達發表於2021-07-19

前言

Flutter 2.0的使用已經有一段時間了,想在此分享一些關於空安全的使用經驗和個人理解。

Dart空安全

空安全是什麼?

在空安全下,執行時的NullPointer Exception錯誤被提前到了開發階段。

即:

void main() {
    // 在空安全下, 開發階段就會報錯,而非執行時
    String name;
    debugPrint(name);
}
複製程式碼

型別上也有了變化,這裡貼兩張官方的型別關係圖深入理解空安全 :

非空安全時代:

alt

空安全時代

alt

可以看到,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() {...}
複製程式碼

開發工具(空安全) 則為這個規範得以被遵守提供了有力的保障(無法編譯)。

這也算是被動提高了開發和使用規範。

一言以蔽之:分鍋更為明確~ 哈哈哈
複製程式碼

結語

最後,謝謝大家的閱讀,如有錯誤歡迎指出。

Dart空安全 官方文件

其他Flutter相關文章

Flutter 仿網易雲音樂App

Flutter&Android 啟動頁(閃屏頁)的載入流程和優化方案

Flutter版 仿.知乎列表的視差效果

Flutter——實現網易雲音樂的漸進式卡片切換

Flutter 仿同花順自選股列表

相關文章