关于Dart编程语言的空安全(null safety),你要知道的都在这里

前言

由于升级了 Flutter 版本,升级完之后跑了一下之前的代码,也没什么问题。升级后最大的区别在于升级后的版本支持 Dart 的 null safety版本了。关于 null safety 其实并不是什么新鲜事了,很早的时候 Swift 就已经支持了,Dart是从2.12.2版本开始支持该特性的。本篇以官方文档为蓝本,聊一下 Dart 的 null safety 特性。官方文档链接:Null Safety

null safety 最大的特点就是默认声明的对象是非空的,除非你明确该对象可以为空

Nullable 和 non-nullable 类型

当你选择使用 null satety 特性时,所有的类型默认是非空的。例如如果声明了一个 String类型的变量,那么就意味着它一直包含字符串值。如果你想要一个 String 对象能够接收字符串值或null,那么就需要在类型声明后面加上?标识,一个声明为String?类型的变量可以包含字符串值或 null

String? str1;
String str2;
// OK
str1 = null;
// 报错
str2 = null;
// OK
List<String?> strList1 = ['a', null, 'c'];
// 报错
List<String> strList2 = ['a', null, 'c'];

空断言操作符!

如果确定一个对象或表达式返回值有值,那么就可以使用空断言操作符!强制转为不为空对象,然后可以使用它赋值给非空对象,或访问其属性或方法,如 valiable!.xx。这种情况下,如果对于nullable 对象不加!,编译器就会报错。但是,如果对象本身是null,加!操作符会导致异常。

int? couldReturnNullButDoesnt() => -3;

void main() {
  int? nullableInt = 1;
  List<int?> intListHasNull = [2, null, 4];

  int a = nullableInt!;
  int b = intListHasNull.first!;
  int c = couldReturnNullButDoesnt()!.abs();

  print('a is $a.');
  print('b is $b.');
  print('c is $c.');
}

类型提升(Type promotion)

为了保证空安全特性,Dart 的流分析(flow analysis)已经考虑了空特性。如果一个 nullable 对象不可能有空值,那么就会被当作非空对象处理,例如:

String? str;

if (str != null) {
  print(str);  //已经确保不为空,不会编译出错
}

late 关键字

有些时候变量、类成员属性或其他全局变量应该是非空的,但是没法在声明的时候直接赋值,这个时候就需要在变量声明的时候加上 late 关键字。当在变量声明的时候加上late关键字后,就是告诉 Dart 如下的内容:

  • 目前还没有给该变量赋值;
  • 我们将在之后才给该变量赋值;
  • 我们保证在使用该变量前肯定会对其赋值。

例如,下面的_description 声明编译器如果没有 late 关键字会报错。

class Meal {
   late String _decription; //错误声明
  
  set description(String desc) {
    _description = 'Meal Description: $desc';
  }
  
  String get description => _description;
}

void main() {
  final myMeal = Meal();
    myMeal.description = 'Feijoada';
    print(myMeal.description);
}

late关键字对处理循环引用还十分有帮助,譬如我们有一个球队和一个教练,球队和教练就存在相互应用的情况。如果没有 late 关键字,我们就只能声明为nullable,那样到时候使用到时候就很别扭了——需要到处加空断言操作符或者使用if来判断是否为空。

class Team {
  late final Coach coach;
}

class Coach {
  late final Team team;
}

void main() {
    final myTeam = Team();
    final myCoach = Coach();
    myTeam.coach = myCoach;
    myCoach.team = myTeam;

    print('搞定!');
}

升级修改

升级修改时,需要根据调用的方法参数、返回值或声明的属性做如下处理:

  • 类属性:非空类属性默认需要由初始值,如果类属性会在别的方法中初始化,那可以加上 late关键字,表示该属性稍后会被初始化,而且是非空的。如果属性可能为空,那么就加上?空标识。这种可为空的属性使用的时候需要特别注意,需要检查是否为空才可以使用,或者使用 variable?.xx这种形式访问,如果明确属性有值,则需要使用!强制指定为非空,如 variable!.xx
  • 方法参数:根据需要设置参数是否是可为空或必传参数,必传的参数加上在参数声明前加上required关键字,可为空的加上?标识。
  • 返回值:如果返回值可能为 null,就在返回参数后加上?标识。如果是集合对象中的某个对象为空,那么需要在集合的类型后加上?标识,例如 List<int?>

对于依赖,也需要修改 pubspec.yaml 文件,包括如下修改:

  • 将依赖最低的 Dart 版本修改为2.12.0
environment:
  sdk: ">=2.12.0 <3.0.0"
  • 修改部分第三方插件依赖,升级到支持null safety 版本,具体可以参考 pub 上的版本说明。

Dio 踩坑

升级完之后,Dio 请求报错DioError [DioErrorType.other]: type 'Null' is not a subtype of type 'Object'。上网搜了,在 issue 里有提到过,但是说是已经解决了。然后按照 issue 里的方法试也不行,后面想了一下,先直接请求百度网页看看是不是 Dio 的问题,结果请求百度网页正常,那就说明是代码自身的问题。最后再定位发现 是我们的 CookieManager 拦截器的请求 headers 设置 Cookie 字段的时候,当_cookienull 的时候导致出现空异常了。这时候我们要检查一下,如果_cookie不为空才设置 Cookie

// CookieManager 之前的代码,_cookie 可能为 null 导致 Dio 报异常
void onRequest(
  RequestOptions options,
  RequestInterceptorHandler handler,
) {
  options.headers['Cookie'] = _cookie;

  return super.onRequest(options, handler);
}

// 修改后
@override
void onRequest(
  RequestOptions options,
  RequestInterceptorHandler handler,
) {
  // null safety后需要不为空才可以设置
  if (_cookie != null) {
    options.headers['Cookie'] = _cookie;
  }
  
  return super.onRequest(options, handler);
}

总结

从编码的角度来说,null safety特性实际上增加了编码的工作量。但是null safety更像是一个强制的约定,要求接口或类明确参数或属性的是否为空,从而可以简化协作,提高代码的健壮性。

当然,对于第三方库来说就需要特别小心,有些第三方库使用的是dynamic 声明的场合,目前 Dartdynamic 声明的变量、属性是不做空校验的,这会导致这样声明的出现空异常,例如上面说到的 RequestOptions optionsheaders,就是一个 Map<String, dynamic>对象,结果使用 null 赋值的时候就会抛出异常。对于这种情况最好是尽量少用 dynamic 声明,同时调用第三方的时候,如果发现有这种情况,需要检查一下是否允许赋值 null

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容