Dart Sound Null Safety 深入分析

Dart Sound Null Safety 深入分析

此文章是学习Understanding null safety的简单总结

类型系统里面的可空特性

null 是非常有用的,可代表为一个不存在的值,但如果不注意就会引起异常。

null 本质是 Null 类的实例, 可视为其他任何类型的子类型,换句话说 int num = null; 实际是多态的一种表现形式。空指针调用方法/属性错误的原因(即NoSuchMethodError 异常)即是来自于调用 null 里面不存在的方法或属性。

image.png

类型系统里的可空与不可空

只要更改 Null 的类型层级结构,没有类型可以直接在为 null, 该变量即为 non-nullable, 可解决空指针问题。

image.png

nullable 类型更像是一个基本数据类型和 Null 的联合体(例如: String?),意味着 Null 会是任何 nullable 类型的子类型。


image.png

结合下面例子, info['familyName'] 取出来将会是一个 null 值, 如果直接转换成 String 那么就会抛 type 'Null' is not a subtype of type 'String' in type cast 结合第一张图可以知晓原因,null 不是 non-nullable 类型的子类型。 但如果转换为 String? 就能够成功运行,结合第二张图,因为 null 是 String? (nullable 类型的子类型)的子类型。

  Map<String, dynamic> info = {'name': 'laozhang'};
  // String familyName = info['familyName'] as String; // error
  String? familyName = info['familyName'] as String?; // ok
  print(familyName);

所有的类型就好像被分割成了两部分,non-nullable 类型侧你可以随意的进行访问方法/属性,不用担心会产生空指针问题。non-nullable 侧的变量可以赋值给 nullable 侧的变量,因为这是安全的,反之则不行。


image.png

顶部与底部类型

在 null-safey 之前 Object 是类型层级树中最顶部的类型,而 Null 则是最底部的类型。
在一个 non-nullable 类型中, Object? 将是其顶部类型而 Never 是其底部类型。
也就是说在 null-safey 中如果要接收一个任意类型的值,使用 Object? 而不是 Object 类型。表示一个底部类型使用 Never 而不是 Null

image.png

特性与分析进行优化与改进

以下优化即是让静态分析变得更聪明了更敏锐了。

返回值

在 null-safety 在函数中如果没有使用 return 返回一个值只会报警告,默认会返回 null。 在 null-safety 中对一个返回值是 non-nullable 类型不允许这么做,必须要有明确的返回值。

变量初始化

以下规则仅限于 non-nullable 类型变量初始化规则如下,原则即是要用之前必须初始化过:

  1. 全局变量和静态变量声明必须要初始化
// Using null safety:
int topLevel = 0;
class SomeClass {
 static int staticField = 0;
}
  1. 实例变量直接声明时初始化或通过构造器来初始化(或使用 late)
class SomeClass {
int atDeclaration = 0;
int initializingFormal;
int initializationList;

SomeClass(this.initializingFormal)
    : initializationList = 0;
}
  1. 局部变量什么时候赋值都可以,但在使用前必须已经赋过值
// Using null safety:
int tracingFibonacci(int n) {
int result;
if (n < 2) {
  result = n;
} else {
  result = tracingFibonacci(n - 2) + tracingFibonacci(n - 1);
}

print(result);
return result;
}
  1. 命名参数必须有默认值或者是 request 关键词修饰。

类型提升

在 null-safety 中解决了对类型提升分析的优化, 如下代码也可以正常运行,Object 实例在 is 判断后会被提升为 List 实例

bool isEmptyList(Object object) {
  if (object is! List) return false;
  return object.isEmpty; // <-- Error!
}

Never

never 可用于表示中断、抛异常。 可作为类型来使用

Never wrongType(String type, Object value) {
  throw ArgumentError('Expected $type, but was ${value.runtimeType}.');
}

赋值

对于以 final 修饰的变量赋值分析变得更加灵活、更加聪明了,以下代码在 null-safety 不再会报错。

// Using null safety:
int tracingFibonacci(int n) {
  final int result;
  if (n < 2) {
    result = n;
  } else {
    result = tracingFibonacci(n - 2) + tracingFibonacci(n - 1);
  }

  print(result);
  return result;
}

null 检查进行类型提升

对 nullable 变量进行 == null 或 != null 等等表达式判断, Dart 会将变量类型提升到 non-nullable 类型, 在对 arguments != null 表达式判断后 List<String>? 提升了为 List<String>

需要注意的是类型提升对类中的字段是没有效果的。因为字段使用太灵活,静态分析无法判断哪里有使用哪里有检查

// Using null safety:
String makeCommand(String executable, [List<String>? arguments]) {
  var result = executable;
  if (arguments != null) {
    result += ' ' + arguments.join(' ');
  }
}
// or
// Using null safety:
String makeCommand(String executable, [List<String>? arguments]) {
  var result = executable;
  if (arguments == null) return result;
  return result + ' ' + arguments.join(' ');
}

多余代码警告(Unnecessary code warnings)

对一个 non-nullable 类型进行 null 检查,如: ?.、 == null 、
!= null 等等静态分析会抛出警告或错误。

// Using null safety:
String checkList(List? list) {
  if (list == null) return 'No list';
  if (list?.isEmpty) { // list?.isEmpty Unnecessary code
    return 'Empty list';
  }
  return 'Got something';
}

Nullable 类型处理

空敏感操作符(null-aware)

在 null-safty 之前,由于不知道调用链中哪个节点可能会出现 null, 所有每个属性/方法调用全部都用可空 ?.

String? notAString = null;
print(notAString?.length?.isEven);

在 null-safty 中, 调用者一旦为 null,那么链中剩下的方法就会被跳过,不会执行,即短路。

thing?.doohickey.gizmo

类似空敏感操作符还有: ?.. ?[]

// Null-aware cascade:
receiver?..method();

// Null-aware index operator:
receiver?[index];

断言操作符(Null assertion operator)

当一个 nullable 变量可以确认它不会为 null 时,可以通过 as 转换或者 ! 来断言其不会为 null。如果转换失败或断言失败则会抛异常,反之即会转换成 non-nullable 类型。

// Using null safety:
String toString() {
  if (code == 200) return 'OK';
  return 'ERROR $code ${(error as String).toUpperCase()}';
}

// Using null safety:
String toString() {
  if (code == 200) return 'OK';
  return 'ERROR $code ${error!.toUpperCase()}';
}

Late 变量

类的 non-nullable 属性或字段在使用前必须要被初始化(在构造器初始化列表或是给默认值都可以),但也可以使用修饰符 late,late 作用将对变量约束从编译时延迟到运行时。但需要注意的是,没有赋值就在运行时使用同样会抛异常。

懒加载

late 还有个作用就是可以对变量懒加载, _temperature 变量不会在构造实例的时候就直接创建而是会延迟到第一次访问这个。如果这个操作_temperature 变量时候。 对于一些消耗大量资源的操作,可以在有需要的时候再进行。

默认上由于实例还没有构造完毕,所以不允许初始化值是实例的方法/属性。但由于是懒加载,这个初始化值现在就可以访问当前实例的方法/属性,比如在 _readThermometer() 方法就是因为 late 关键字所以可以访问。

// Using null safety:
class Weather {
  late int _temperature = _readThermometer();
}

Late final 结合

含义即是: 对于 non-nullable 变量不需要在声明时或构造器进行初始化且必须赋值有且只有一次。

Required 修饰符

防止 non-nullable 命名参数为 null,类型检查要求命名参数必须使用 required 修饰或者给参数默认值。

nullable 字段的处理

上文讲过类型提升对类中的字段是没有效果的如下代码,编译器会报错:

class Coffee {
  String? _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }

  void checkTemp() {
    if (_temperature != null) {
      print('Ready to serve ' + _temperature + '!');
    }
  }

  String serve() => _temperature! + ' coffee';
}

要处理有几种方案,一种是直接对 _temperature 加断言操作符 !。另外就是将变量先拷贝成局部变量再做类型提升,如果局部变量改了值要记得改赋值回字段。

// Using null safety:
void checkTemp() {
  var temperature = _temperature;
  if (temperature != null) {
    print('Ready to serve ' + temperature + '!');
  }
}

泛型的可空性

T 可以是 nullable 类型也可以是 non-nullable 类型。

// Using null safety:
class Box<T> {
  final T object;
  Box(this.object);
}

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

推荐阅读更多精彩内容