likes
comments
collection
share

Dart Sound Null Safety 深入分析

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

Dart Sound Null Safety 深入分析

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

类型系统里面的可空特性

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

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

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

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

 Dart Sound Null Safety 深入分析

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

结合下面例子, 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 侧的变量,因为这是安全的,反之则不行。  Dart Sound Null Safety 深入分析

顶部与底部类型

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

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

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

返回值

在 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);
}