尝鲜 Dart 2.7 最新语法之可空与非空类型

Posted 熊喵先生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了尝鲜 Dart 2.7 最新语法之可空与非空类型相关的知识,希望对你有一定的参考价值。

从这篇文章开始将进入 Dart 2.7 新特性的世界。我特别想用 Dart 官方一句话来描述 Dart 2.7:

A safer, more expressive Dart.(更安全,更具表现力的 Dart)

因为在 Dart 2.7 添加了对扩展方法的支持,以及一个用于处理带有特殊字符的 characters package(preview)。此外,在 DartPad 中更新了对空安全(preview,类型安全可为可空和非空的类型)支持。

为什么更安全?

如果学过 Java 的编程语言就知道,我们经常会遇到 NPE(NullPointerException)的问题(俗称:空指针运行时异常)。然后学过 Kotlin 也会知道,Kotlin 相比 Java 最大优点之一就是可以极大避免 NPE 的问题,可是有人是否去思考为什么 Kotlin 能够很好解决 NPE 的问题。

其实 Kotlin 能够解决 NPE 的问题,本质是 Kotlin 对它的类型系统做了非空和可空类型的划分。比如定义一个非空的变量,若没有初始化则会静态提示报错,只有选择延迟初始化或者立即初始化报错提示才会消失,那么在使用这个变量的使用就能确保它是初始化过的,所以自然不可能出现空指针问题;比如定义一个可空的变量时,当在使用这个可空变量时,IDE 就会提示你需要对它做判空处理,这样一来自然也就不可能出现空指针问题了。

然而 Dart 的可空与非空类型和 Kotlin 的目的是一样的。因为在 Dart 没可空非空划分之前,Dart 和 Java 是一样:定义一个变量后不立即初始化,编译器不会报错;然后开发者随着后面代码逻辑冗长和复杂,忘记了初始化直接使用这个变量,然而这在编译期也无法检测到,一旦运行时执行就抛出异常。

未加入对 non-nullable 支持的 Dart 版本(目前稳定正式版本):

运行结果:

加入对 non-nullable 支持的 Dart 版本(目前还是 experimental 中):

那么通过上面 IDE 编译期静态检查就能很好提醒开发者,在使用 name 变量前需要被初始化。这样以致于不会把异常抛到运行时。

为什么更具表现力?

因为新增了扩展方法的支持,也就意味着可以任意扩展你想要扩展类的方法。何为扩展方法,用一句话概括:它能实现对一个已经存在的类(这个类可能存在于不可修改的库中)扩展新增对应方法,然后在使用的过程中就像是直接调用这个类的成员方法一样。举个例子:

  void main() 
    String lowercase = "ADsfc".toLowerCase();//toLowerCase 是 String 类成员方法,可以直接使用 .toLowerCase() 直接调用
    print(lowercase);

    int intValue = '200'.toInt();//然而 toInt() 并不是 String 类的成员方法,但是由于它是一个 String 类扩展方法,可以类似成员方法那样调用
    print(intValue);

    double doubleValue = '3.1415926'.toDouble();//然而 toDouble() 也不是 String 类的成员方法,但是由于它是一个 String 类扩展方法,可以类似成员方法那样调用
    print(doubleValue);
  

  //定义一个在 String 类中的扩展
  extension ParseNumbers on String 
    int toInt() 
      return int.parse(this);
    

    double toDouble() 
      return double.parse(this);
    
  

1. 为什么需要加入 non-nullable 的支持?

加入 non-nullable 的支持,可以更加健全 Dart 的类型系统;从而使得类型系统更加安全,很大避免了类似 Java 中 NPE 问题,在 Dart 上则是对一个 Null 调用方法,会表现是 NoSuchMethodError 的异常。

从 Dart 官方路线来看,Dart 要支持 non-nullable 特性从很早之前就开始了,一直处于计划中,用官方的话说:

由于 Dart 和 Kotlin、C#、Swift 一样也没有原语类型或值类型,因此 non-nullable 可以提供性能上的优势。具体参考:

https://github.com/dart-lang/language/blob/master/accepted/future-releases/nnbd/roadmap.md

2. 加入 non-nullable 后对开发有什么影响?

通过上面介绍我们知道,目前 Dart 版本并没有正式支持 non-nullable,所以在定义一个变量的时候,即使不对它初始化也不会出现静态提示错误。

2.1 当在声明定义变量时,一定要做初始化或者延迟初始化

比如现在的 Dart 你可以这样:

但是加入 non-nullable 后你会得到静态提示错误:

解决方法:

  • 声明时立即初始化

  • late 关键字实现延迟初始化

所以,总结一下最大影响在于:在加入 non-nullable 的支持后,当在声明定义变量的时候,一定要做初始化或者延迟初始化(一般使用 late 关键字,后面会详细讲解,但是需要注意的是使用 late 延迟初始化,使用该变量前一定要确定在这之前要初始化,否则也会抛出异常)。

2.2 当在声明定义某个变量时,一定要先考虑当前变量是可空还是非空

在声明一个可空类型变量的时候,就需要在使用的时候手动判空处理;声明一个非空变量的时候就会提示是否初始化或延迟初始化。

3. 官方对 non-nullable 支持进度

3.1 GitHub 最新 commit 的进度

其实,官方一直在致力于 non-nullable 特性的支持,通过最近 GitHub 提交的记录就能看出,随便抽取最近的 commit:

3.2 SDK 源码层面

如果对 Dart SDK 源码感兴趣的小伙伴,可以发现官方已经偷偷在造另一个 SDK 了,那就是 sdk_nnbd。

nnbd 全称就是 non-nullable by defalut,也就是这个 SDK 将在后续版本支持可空与非空类型。

注意:随着 nnbd 越来越完善,将采用逐步将 sdk_nnbd 替代 SDK,不妨来看下它们包结构对比,几乎都是一样的。

4. non-nullable 语法新特性介绍

4.1 空类型声明符 ?

在 Dart 中声明可空类型声明符和 Kotlin 是一样的,都是通过 ? 后跟在类型后面。

//kotlin 声明可空类型
class Student 
    var name: String?//使用 ? 表示可空类型
    val age: Int

//dart 声明可空类型
class Student 
  String? name;//静态检查通过,使用 ? 表示可空类型,String? 则是可空类型 String
  int age; //静态检查错误

如果可空类型变量 name,在它使用之前没有被初始化,那么它会被默认初始化为 null。

void main() 
    String? name;
    print(name); // output null

4.2 非空断言 !

有时候我们会遇到一个场景,就是经过判空处理,编译器无法智能分析出是否进行过非空判断,这时候它还是会提示你去进行判空处理,这时候就没必要去再判断一次,你可以使用非空断言 !,强行告诉编译器这是一个不为空的变量。

注意:使用非空断言的变量必须保证确实不为 null,否则会抛出异常。比如下面的例子,编译器是无法推断出已经做了非空判断的:

这时候就可以使用 ! 非空断言,来告诉编译器,在确定逻辑分支内,它是不可能为空的。

注意:Dart 中的非空断言是 ! 单感叹号,而 Kotlin 是 !! 双感叹号。

//dart 非空断言
void main() 
  String? name;
  if (isNotNull(name)) //其实已经做了判空处理,运行实际上是 OK 的,
    // 但是编译器无法智能推断是否进行判空,所以还是提示报错
    print(name!.length);
   else 
    print(0);
  


bool isNotNull(String? param) 
  return param != null;

//Kotlin 非空断言
fun main(args: Array<String>) 
    var name: String?
    if (isNotNull(name)) 
        println(name!!.length)//!! 非空断言
     else 
        println(0)
    


fun isNotNull(param: String?): Boolean 
    return param != null

4.3 late 延迟初始化

late 关键字主要用于延迟初始化,从前面知道 Dart 加入 non-nullable 对于非空类型变量需要做初始化,初始化主要分为两种:声明处默认值初始化和延迟初始化。但是并不是所有场景都合适使用声明处默认值初始化,比如下面这个场景:

void main() 
  late Student student; //对于非内置基本数据类型,一般建议采用 late 延迟初始化,
  // 如果开始初始化需要创建一个默认的 Student 对象,这是不妥的。所以这时候 late 延迟初始化就派上用场了。
  student = createStudent('mike', 28);
  String name = "";//对于内置基本数据类型,如果没有严格延迟初始化,可以采用直接赋默认值初始化,如果有严格延迟初始化,就用 late
  double doubleValue = 34.5;
  int intValue = 100;


Student createStudent(String name, int age) 
  return Student(name, age)


class Student 
  String name;
  int age;

  Student(this.name, this.age);

注意:late 延迟初始化使用时,当使用这个变量时,一定要确保在使用之前要初始化,否则抛出异常。

注意:Dart 延迟初始化是使用 late 关键字,而在 Kotlin 中则使用 lateinit 关键字。

//dart late 关键字
void main() 
  late Student student;//dart 使用 late 关键字,可以直接用于顶层函数 main


class Student 
  String name;
  int age;

  Student(this.name, this.age);

//kotlin lateinit 关键字
class Test 
    private lateinit var student: Student//使用 lateinit 进行延迟初始化,
    // 但是 Kotlin 不支持在顶层函数中使用 lateinit,而 Dart 可以
    init 
        initStudent()
    

    fun initStudent() 
        //do logic
    


data class Student(
    val name: String,
    val age: Int
)

4.4 late final 用于 final 变量延迟初始化

late final 关键字主要用于常量延迟初始化,和 late 一样都是用于延迟初始化,但是 late final 只能被赋值一次。

late final int number;//声明顶层延迟初始化 final 变量
number = 100;//合法
number = 200;//非法

注意:现在所有顶层变量或静态变量都是延迟初始化,即使没有显示加上 late 关键字。

4.5 required 关键字

最开始 @required 是注解,现在它已经作为内置修饰符。主要用于允许根据需要标记任何命名参数(函数或类),使得它们不为空。因为可选参数中必须有个 required 参数或者该参数有个默认值。

void testFunc(required String param) //required 修饰符
    //do logic

//或者命名参数加默认值
void testFunc(String param = 'defalut value') //默认值
 //do logic

//或者命名参数为可空类型
void testFunc(String? param) 
 //do logic

4.6 ?[] 运算符

?[] 主要为了给索引运算符 [] 添加空判断的能力。

void main() 
    List<int>? numbers = [1, 2, 3];
    int? value = list?[0];  //value: 1

4.7 ?.. 级联运算符

级联运算符可以有新的判空运算符 ?..,在它以下级联操作仅在接收者不为 null 时执行。所以 ?.. 必须是级联序列中的第一级运算符:

void main() 
  Path? path;
  path
    ?..moveTo(3, 4)
    ..lineTo(4, 3);

  (null as List)
    ?..add(4)
    ..add(2)
    ..add(0);

4.8 ?? 运算符(result = expr1 ?? expr2)

?? 运算符表示 ?? 前面表达式值为 null 时,就执行后面表达式,或者可以把后面表达式赋值。如果发现 expr1 为 null,就返回 expr2 的值,否则就返回 expr1 的值,这个类似于 Kotlin 中的 result = expr1 ?: expr2

main() 
    var choice = question.choice ?? 'A';
    //等价于
    var choice2;
    if(question.choice == null) 
        choice2 = 'A';
     else 
        choice2 = question.choice;
    

4.9 Never 类型

Never 类型类似于在 dart:core 包中先前定义的 Null 类型(注意不是 null),它们都不能被继承(extend)、被实现(implement)、被混合(mixin),所以它们不能被扩展使用。

本质上,Never 意味着不允许任何类型,且不能将 Never 类本身实例化。只不过 Never 作为泛型存在于 List<Never> 中,意味着 List 必须为空集合,然而 List<Null> 表示集合中元素必须都是 null。

final neverList = <Never>[//[] 集合只能为空,里面不能有任何的值,否则会报错
  100,//检查报错
  null,//检查报错
  Never//因为 Never 不是一个值所以,编译期会报错
];

final nullList = <Null> [//[] 集合只能为 null,不能含有非 null 的值,否则会报错
  100,//检查报错
  null,//检查正常
  null//检查正常
]

5. 如何提前尝鲜使用 non-nullable?

由于现在 non-nullable 还是处于 experiment 中,所以很多人不知道如何配置使用。这里给出两个尝鲜教程,可以提前帮助你去学习 non-nullable 语法特性。

5.1 使用 Dart 2.7 的 DartPad 来尝鲜

官方宣称已经在 DartPad、Dart 2.7 版本可以体验 non-nullable,具体的体验地址:

https://nullsafety.dartpad.dev/

5.2 配置 IDE 支持 non-nullable experiment

是不是用 DartPad 去学习可空和非空总觉得怪怪的,包括从 GitHub、Stack Overflow,有很多小伙伴想尝试在 IDE 中配置支持 non-nullable,可惜他们找了很久都没有找到很好的配置方案。

因为配置支持 non-nullable 还和 Dart SDK 的版本有关。下面我将带大家如何正确配置支持 non-nullable,开始划重点了:

首先,准备一个 2.8 的 dart-sdk,SDK 版本最好是在 2.8.0-dev9.0 之上的版本,我这里就以 2.8.0-dev9.0 版本为 SDK。dart-sdk 下载地址:

https://dart.dev/tools/sdk/archive

选择 Dev channel 下载,选择对应版本和 OS 平台即可。

然后,将下载好的 2.8.0-dev9.0.zip 文件解压到指定目录,打开 IDE(这里以 IntelliJ IDEA 为例),配置 Dart SDK 路径,会自动识别当前 Dart SDK 版本。

然后,用 IDE 新建一个 Dart Command 项目,在 bin 目录再建一个 nnbd.dart 文件,并在文件中定义一个可空类型,未配置 non-nullable 的会直接提示报错。

添加 analysis_options.yaml 文件,来使得 IDE 的静态检查支持 non-nullable 的 experiment 的特性,并在 analysis_options.yaml 添加如下代码:

  analyzer:
    enable-experiment:
      - non-nullable

添加 analysis_options.yaml 文件后,此时 IDE 就不会提示报错,此时应该完成其中一半那就是静态检查可以正常通过。

如果此时直接运行该代码是会抛出异常:

为了解决 Dart VM 运行支持 non-nullable,还需要添加 VM options 参数。所以只需要在 Dart 执行命令中添加 --enable-experiment=non-nullable 即可。

  # 直接命令编译
  dart --enable-experiment=non-nullable bin/nnbd.dart

为了更好在 IDE 上运行,可以直接在通过 Edit Configuaration 来配置 VM options 参数。

最后,直接可以运行代码,这样就可以直接通过 IDE 来体验 non-nullable 的功能。

6. 总结

到这里,有关 Dart 2.7 第一个新特性可空与非空类型就结束了,可以看到 Dart 官方正在致力于重构划分 Dart 的类型系统。 nnbd_sdk 正在筹建中,慢慢后续可能需要开发者从原来的 SDK 迁移到 nnbd_sdk 中,所以对于我们而言提前掌握它的语法特性很有用。

下一篇我们将进行 Dart 另一个具有表现力的语法新特性:扩展方法。

以上是关于尝鲜 Dart 2.7 最新语法之可空与非空类型的主要内容,如果未能解决你的问题,请参考以下文章

尝鲜 Dart 2.7 最新语法之扩展方法

尝鲜 Dart 2.7 最新语法之扩展方法

尝鲜 Dart 2.7 最新语法之泛型强化:声明处型变

尝鲜 Dart 2.7 最新语法之泛型强化:声明处型变

[译] Dart中可空性语法的定案: a?[b] 或 a?.[b]

Kotlin系列之可空类型的处理