原文(节选):Announcing Dart 2.7: A safer, more expressive Dart
翻译:老黑牛
声明:侵权联删
Dart 2.7 添加了对扩展方法的支持,同时新增了一个处理特殊字符的包。在null
安全方面我们也做了更新,并在DartPad中带来了全新的null
安全体验。在生态系统方面,pub.dev添加了一个新的Like
特性:你可以给你喜欢的packages反馈。今天,Dart2.7 SDK可以从dart.dev下载,而且Dart2.7 SDK也被内嵌入了Flutter 1.12 release版本中。
扩展方法
Dart 2.7添加了一个期待已久的,功能强大的新语言特性:extension methods.这使您能够向任何类型添加新功能——甚至是您无法控制的类型——并具有常规方法调用的简洁性和自动完成的体验。
让我们来看一个小例子----给String
添加转化 ints 和 doubles 的支持。作为app开发人员,我们不能改变 String
,因为它是声明在 dart:core
库里的,但是我们可以使用扩展方法来扩展它!一旦我们定义了这个拓展,我们可以在String
类型上调用我们的新方法parseInt
,就像String
类型自己定义的那样。
extension ParseNumbers on String {
int parseInt() {
return int.parse(this);
}
double parseDouble() {
return double.parse(this);
}
}
main() {
int i = '42'.parseInt();
print(i);
}
扩展方法是静态的
扩展方法被静态的解析和分发,这就意味着你不能在dynamic
类型的变量上面使用扩展方法。下面这个调用在运行时会抛出异常:
dynamic d = '2';
d.parseInt();
→ Runtime exception: NoSuchMethodError
扩展方法可以很好地与Dart的类型推断一起使用,因此在下面的变量v
可以推断出类型为String,而且String
的扩展是它也是能使用的:
var v = '1';
v.parseInt(); // Works!
扩展可以有类型变量
想象一下我们想给List
声明一个扩展,这个扩展方法可以获取每一个索引对应的元素。我们想让这个扩展方法可以在任何类型的list上都能工作,想让它返回一个新的并且和输入的list相同类型的list。我们可以通过使这个拓展范型化,并将其类型参数应用给它扩展的类型和扩展方法来实现这一点:
extension FancyList<T> on List<T> {
List<T> get evenElements {
return <T>[for (int i = 0; i < this.length; i += 2) this[i]];
}
}
扩展方法实际上是扩展属性
我们之所以称这个特性为扩展方法,是因为如果您在其他编程语言中使用过相应的语言特性,那么这是一个熟悉的术语。但是在Dart中,该特性更加通用:它还支持getter
、setter
和运算符
的扩展。在上面的FancyList
示例中,evenElements
就是是一个getter
的扩展。下面是一个为了移位字符串而添加运算符的例子:
extension ShiftString on String {
String operator <<(int shift) {
return this.substring(shift, this.length) + this.substring(0, shift);
}
}
来自社区的优秀例子
我们已经看到过一些来自Dart社区的开发者使用扩展方法做了些实验。这有一些目前我们看到的很酷的使用。
Jeremiah Ogbomo创建了 time package,可以在num
(ints
和 doubles
的基类)上面使用这些扩展来简化 Duration
对象的创建:
// Create a Duration via a `minutes` extension on num.
Duration tenMinutes = 10.minutes;
// Create a Duration via an `hours` extension on num.
Duration oneHourThirtyMinutes = 1.5.hours;
// Create a DateTime using a `+` operator extension on DateTime.
final DateTime afterTenMinutes = DateTime.now() + 10.minutes;
Marcelo Glasberg创建了i18n (internationalization) package,可以用来简化字符串的本地化操作:
Text('Hello'.i18n) // Displays Hello in English, Hola in Spanish, etc.
Simon Leier 创建了 dartx package,它包含了一些核心Dart类型的扩展。一些例子:
var allButFirstAndLast = list.slice(1, -2); // [1, 2, 3, 4]
var notBlank = ' .'.isBlank; // false
var file = File('some/path/testFile.dart');
print(file.name); // testFile.dart
print(file.nameWithoutExtension); // testFile
Brian Egan 利用扩展方法正在更新流行的RxDart package 的相关API,重新定义它们为了可以使用streams。
安全的截取字符串处理
Dart的标准字符串类使用UTF-16编码。这在编程语言中是一种常见的选择,尤其是那些既支持在设备上本地运行又支持在web上运行的语言。
UTF-16字符串通常工作得很好,而且编码对开发人员是透明的。然而,在操作字符串时,特别是在操作用户输入的字符串时,您可能会体验到用户所感知到的字符与UTF-16中编码为代码单元的字符之间的差异。让我们来看一个小例子,提取用户输入的字符串的前三个字符:
var input = ['Resume'];
input.forEach((s) => print(s.substring(0, 3)));
$ dart main.dart
Res
到目前为止没有问题;我们在输入列表中打印了字符串的前三个字符,结果是Res.现在让我们考虑来自不同地区的用户,他们可能会输入包含口音、韩文(韩语)甚至是代表“resume”概念的表情符号组合的字符串:
// New longer input list:
var input = ['Resume', 'Résumé', '이력서', '💼📃', 'Currículo'];
$ dart main.dart
Res
Ré
이력서
💼�
Cur
嗯,一些是正常的,但是对于Résumé
和💼📃
而言发生了什么呢?对于Résumé
而言,为什么我们得到了两个字符?对于💼📃
,这个奇怪的问号又是怎么回事?这里的问题在于dark corners of Unicode.Résumé
里的重音 é
确实占两位: e
和 组合的重音. 至于 📃
, 一个卷纸表情,是一个刚好用U+d83d U+dcc3的代理对进行编码的单一代码点。困惑吗?
正如我们所说的,通常不需要担心字符和代码点。如果您所做的只是接收、分发和传递整个字符串,那么内部编码是透明的。但是,如果需要遍历字符串的字符或操作字符串的内容,就会遇到麻烦。好消息是,Dart 2.7引入了一个新的包—— characters 用于处理这些情况。这个包支持将字符串视为用户感知字符的序列,也称为Unicode字符簇。有了字符包,我们可以在截短代码上做一点改变就能修复上面的问题:
// Before:
input.forEach((s) => print(s.substring(0, 3)));
// After, using the characters package:
input.forEach((s) => print(s.characters.take(3)));
首先,我们从s
中的字符串创建一个新的Characters
实例(使用方便的.characters
扩展方法)。然后我们使用灵巧的take()
方法来提取初始的三个字符。
这个新包的技术预览版可以在pub.dev上找到。我们很想听听你对这个包的看法。如果您发现任何问题,请提出来。
空安全预览
几个月前,我们宣布了在Dart中支持空安全的意图,增加了对安全访问对象引用而不触发空引用异常的支持。今天,我们将为您提供一种预览空安全静态分析的方法。让我们来看看一个激励人的小例子:
void main() {
Person('Larry', birthday: DateTime(1973, 03, 26)).describe();
Person('Sergey').describe();
}
class Person {
String firstName;
DateTime birthday;
Person(this.firstName, {this.birthday});
void describe() {
print(firstName);
int birthyear = birthday?.year;
print('Born ${DateTime.now().year - birthyear} years ago');
}
}
如果我们运行这段代码,当描述第二个人的时候会因为空指针异常而崩溃,因为第二个人没设置生日信息。我们犯了一个编码错误:当我们通过将构造函数中的birthday
属性设置为可选并且使用birthday?.year
来测试空的生日属性的时候,是能预见到某些人可能有未知的生日,我们忘记了去处理birthday
为null的场景。
让我们把这些代码粘贴到我们新的null safety playground,,它是一个具备空安全静态分析预览技术的特殊DartPad构建。甚至不用运行代码,我们可以看到三个issues:
通过修复这些分析错误,我们可以开始利用null安全性。尝试在null safety playground进行以下编辑(最终得到这个安全代码):
- 声明生日属性或许为空,将
DateTime birthday
改成DateTime? birthday
- 声明birthyear属性或许为空,当birthday为空时,将
int birthyear
改为int? birthyear
- 使用空探测包装最后的输出调用:
if (birthyear != null) {…}
我们希望这个例子能很好地说明我们想要的null安全体验。如前所述,这个playground只是null安全的一部分的早期技术预览,因为它正在建设中。我们正在努力在Dart SDK中完成第一个null安全beta版。这是我们正在做的beta版:
-
完成可空引用和不可空引用的完整实现
-
将null安全性集成到Dart的类型推断和智能提升中(例如,允许在赋值或空值检查后安全访问可为空的变量)
-
移植Dart核心库来声明哪些类型可以为空,哪些类型不能为空
-
添加一个迁移工具,它可以自动完成移植Dart应用程序和包的大部分升级任务
一旦这项工作完成,我们将在beta SDK中提供它,您在您的app和包中使用这个特性。我们也计划保持空安全的playground的更新,当新特点被实现完成后。
同时我们确信许多开发者将想使用null safety一旦他可以获得的时候,您可以在任何方便的时候进行迁移,并在准备好时选择使用该特性。尚未加入该特性的库和包将能够依赖于已加入的库,反之亦然。
在接下来的几个月里,我们将更多地讨论null安全性,包括关于如何准备转换的更详细的建议。