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

Posted Android开发骆驼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了大佬谈关于Flutter空安全的一些使用经验和理解相关的知识,希望对你有一定的参考价值。

前言

Flutter 2.0的使用已经有一段时间了,在某平台看到一位大佬分享的关于Flutter空安全的一些使用经验和理解,我看完也觉得很有共鸣。
原文链接:https://juejin.cn/post/6986455348026540068

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() ...
复制代码

而 开发工具(空安全) 则为这个规范得以被遵守提供了有力的保障(无法编译)。
这也算是被动提高了开发和使用规范。

一言以蔽之:分锅更为明确~ 哈哈哈
复制代码

最后

我把自己这段时间整理的android最重要最热门的学习方向资料放在了我的GitHub,里面还有不同方向的自学编程路线、面试题集合/面经、及系列技术文章等。

资源持续更新中,欢迎大家一起学习和探讨。

以上是关于大佬谈关于Flutter空安全的一些使用经验和理解的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

前端大佬谈 React Fiber 架构

关于MockView中“Mock“编程思想的个人理解及实践

关于MockView中“Mock“编程思想的个人理解及实践