likes
comments
collection
share

关于Flutter空安全的一些使用经验和理解

作者站长头像
站长
· 阅读数 31

前言

Flutter 2.0的使用已经有一段时间了,想在此分享一些关于空安全的使用经验和个人理解。

Dart空安全

空安全是什么?

在空安全下,运行时的NullPointer Exception错误被提前到了开发阶段。

即:

void main() {
    // 在空安全下, 开发阶段就会报错,而非运行时
    String name;
    debugPrint(name);
}

类型上也有了变化,这里贴两张官方的类型关系图深入理解空安全 :

非空安全时代:

关于Flutter空安全的一些使用经验和理解

空安全时代

关于Flutter空安全的一些使用经验和理解

可以看到,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版 仿.知乎列表的视差效果

Flutter——实现网易云音乐的渐进式卡片切换

Flutter 仿同花顺自选股列表