Loading... ## 引入 空安全在现代编程语言中并不是 Dart 的专利,像其他的一些语言,比如 Kotlin,TypeScript,C# 等都有自己的解决方案,编译时的空安全对于开发者来说不一定是必要的,但对于开发效率的提升是不可否认的。 ## 非空与可空 <div class="tip inlineBlock share simple small"> 除非你将变量显式声明为可空,否则它一定是非空的类型 </div> 在引入了空安全后,所有的类型都是默认非空的,假如声明了一个 `String` 类型的变量,那么它必须是非空的,如果想要声明一个可以为空的变量,需要在类型声明后加上 `?` ,就像下面这样: ```dart //非空 String str = "not null"; //可空 String? nullStr = null; //报错 String wrong = null; ``` 本质上,这样的写法和定义了一个原有类型加上 `Null` 的联合类型没有什么区别,如果 Dart 包含完整的联合类型定义,那么 `String?` 本质上就是 `String|Null` 的缩写。 ### 空类型的提升 可空变量的出现虽然解决了空安全的问题,但是也会带来其他的问题,就拿上面的例子来说,当你将 `nullStr` 这个变量定义为 `String?` 时,你无法对它做任何有用的事情,就算它可能为 `String` 类型,你也不能够直接调用 `String` 类下的方法,比如 `.length` 来获取它的长度。不过不用担心,Dart 的静态分析提供了优雅的解决方案。 如果你已经判断了一个可空的变量不为空,下一步后 Dart 就会将这个变量的类型提升至对应的非空类型。 ```dart // Using null safety: String makeCommand(String executable, [List<String>? arguments]) { var result = executable; if (arguments != null) { result += ' ' + arguments.join(' '); } return result; } ``` 在这段代码中,`arguments` 是一个可空的类型,通常情况下对其调用 `.join()` 是禁止的。但是,由于 `if` 语句已经判断值不为空,Dart 就会将它的类型从 `List<String>?` 提升到 `List<String>` 从而让你可以调用它的方法,当然 Dart 足够智能,上面的代码也可以像下面这样编写: ```dart // Using null safety: String makeCommand(String executable, [List<String>? arguments]) { var result = executable; if (arguments == null) return result; return result + ' ' + arguments.join(' '); } ``` 实际上,除了显式的 `== null` 和 `!= null` 以外,对其赋值,以及使用下面要讲的 `!` 或 `as` 都会进行类型提升,需要注意的是,类型提升最初适用于局部变量,从 Dart 3.2 开始也适用于私有 final 字段。 ### 空值断言操作符 将一个可空变量提升为非空变量是很有必要的,你可以在先前的可空变量上调用对应非空类型的方法,还可以享受非空类型带来的安全和性能优势,Dart 中强大的静态分析功能可以帮你解决一部分问题,但是,有些可空类型的使用方法,不能向静态分析证明它们的安全性,也就意味着 Dart 无法自动提升它们的类型,例如下面的代码: ```dart // Using null safety, incorrectly: class HttpResponse { final int code; final String? error; HttpResponse.ok() : code = 200, error = null; HttpResponse.notFound() : code = 404, error = 'Not found'; @override String toString() { if (code == 200) return 'OK'; return 'ERROR $code ${error.toUpperCase()}'; } } ``` 如果你尝试运行这段代码,编译器会给出一个来自 `toUpperCase()` 调用的编译错误,因为编译器认为 `error` 可能为空,但实际上,通过观察可以发现,当 `error` 为空时,我们永远不会访问它。但要知道这个行为,必须要理解 `code` 和 `error` 之间存在的联系,作为人类我们可以很容易知道这一点,但是类型检查器做不到。 为了应对这一情况,通常我们可以使用 `as` 来转换断言类型,就像这样: ```dart // Using null safety: String toString() { if (code == 200) return 'OK'; return 'ERROR $code ${(error as String).toUpperCase()}'; } ``` 这里我们将可空的类型转换为了 `String`,当 `error` 为 `null` 时,编译器会抛出一个异常,如果转换成功,`error` 就会变为一个字符串供我们使用。 虽然 `as` 加上对应类型的操作可以非常直观的断言一个可空的类型,但是如果面对一个复杂的类型,就会变得非常繁琐,假如有一个这样的类型 `Map<TransactionProviderFactory, List<Set<ResponseFilter>>>`,不烦吗? 为了应对这种情况,Dart 提供了一个操作符 `!` 来将一个可空的类型断言为非空,上面的示例就可以变成这样,是不是更加优雅了。 ```dart // Using null safety: String toString() { if (code == 200) return 'OK'; return 'ERROR $code ${error!.toUpperCase()}'; } ``` ## 延迟初始化 对于顶层的变量和字段,类型检查器通常无法证明其是否安全。这里有一个例子: ```dart // Using null safety, incorrectly: class Coffee { String _temperature; void heat() { _temperature = 'hot'; } void chill() { _temperature = 'iced'; } String serve() => _temperature + ' coffee'; } main() { var coffee = Coffee(); coffee.heat(); coffee.serve(); } ``` 在这段代码中,`heat()` 在 `serve()` 之前被调用了,虽然这里对于 `_temperature` 初始化是合法的(对于人类而言),但就像在上面的例子一样,类型检查器无法跟踪这样的状态。所以在这里,Dart 会给出一个编译错误。 为了解决这个问题,你可以将顶层变量声明为可空,然后使用空断言操作符来使用它,就像这样: ```dart // Using null safety: class Coffee { String? _temperature; void heat() { _temperature = 'hot'; } void chill() { _temperature = 'iced'; } String serve() => _temperature! + ' coffee'; } ``` 这样一来,虽然代码可以正常工作了,但是又带来了一个新的问题。在这里将 `_temperature` 定义为了一个可空类型,仅仅是为了消除编译错误,但实际上 `_temperature` 永远不会为空。 为了处理类似延迟初始化的问题,Dart 新增了一个修饰符 `late`,现在你可以这样使用: ```dart // Using null safety: class Coffee { late String _temperature; void heat() { _temperature = 'hot'; } void chill() { _temperature = 'iced'; } String serve() => _temperature + ' coffee'; } ``` 此处 `_temperature` 作为非空的类型,但是并没有进行初始化。同时,使用时也没用明确的空断言,在这里 `late` 关键字意味着“在运行时而非编译时对变量进行约束”,就好像在对编译器说:”每次运行时都要检查它是不是真的“。 `late` 关键字还有一些其他的功能,虽然听起来很奇怪,但是你可以在一个包含初始化的字段上使用 `late`,就像这样: ```dart // Using null safety: class Weather { late int _temperature = _readThermometer(); } ``` 这样声明会使初始化延迟执行。实例的构造将会延迟到字段首次被访问时才执行,而不是在被构造时就被初始化,换句话说,它让一个普通字段的初始化方式变得和顶层字段一样了!当初始化表达式非常消耗性能,并且有可能不需要时,他会变得非常有用。 ## 可空性和泛型 和主流的静态类型语言一样,Dart 中也有泛型类和泛型方法。当引入了泛型之后,可空与非空就不再是一个简单的是非问题,看下面的例子: ```dart // Using null safety: class Box<T> { final T object; Box(this.object); } main() { Box<String>('a string'); Box<int?>(null); } ``` 在 `Box` 的定义中,`T` 是一个可空还是非空的类型?作为一个泛型,它可以通过任意一种类型来进行实例化,实际上 `T` 是一个**潜在的可空类型**,这样的类型会包含可空类型和非空类型的所有限制,前者意味着你只能调用定义在 `Object` 上的少量方法,后者意味着这个类型的所有字段或者变量都要在使用前被初始化。听起来就非常麻烦。 实际上,在使用过程中,我们只需要在使用过程中用合适的方式处理类型相关的约束即可,在你不需要访问值的时候,你可以将类型变为可空: ```dart // Using null safety: class Box<T> { T? object; Box.empty(); Box.full(this.object); } ``` 当你将类型参数变为可空时,你可能需要强制将其转换为非空类型,正确的做法是显式使用 `as T` 来进行转换,而不能使用 `!`。 这是因为,如果值为 `null` 使用 `!` 一定会抛出异常。但是如果类型参数已经被声明为一个可空的值,那么 `null` 对于 `T` 来说就是完全合法的值。 © 允许规范转载 打赏 赞赏作者 微信 赞