作者/MichaelThomsen
Dart2.12现已发布,其中包含健全的空安全和DartFFI的稳定版。空安全是我们最新主打的一项生产力强化功能,意在帮助您规避空值错误,以前这种错误通常很难被发现,您可以观看下面这支视频了解详情。FFI则是一种互操作机制,支持调用以C语言编写的既有代码,例如调用WindowsWin32API。欢迎大家即刻开始使用Dart2.12。
05:39Dart平台的独特功能
在详细了解健全空安全和FFI之前,我们先来讨论一下它们在哪些方面契合了我们对Dart平台的期望。编程语言往往有很多类似的功能,例如,很多语言都支持面向对象的编程或在web上运行。真正将各个语言区分开来的,是其独特的功能组合。
Dart具有横跨三个维度的独特功能组合:
可移植性:高效的编译器可针对设备生成x86和ARM机器代码,并针对web生成优化的JavaScript。同时兼容移动设备、桌面PC、应用后端等多种目标平台。大量的开发库和package提供了可在所有平台上使用的一致的API,进一步降低了开发者创建真正多平台应用的成本。高生产力:Dart平台支持热重载,因此可在原生设备和web上实现快速迭代开发。此外,Dart还提供了丰富的结构,如isolates和async/await等,用以处理和实现常见的并发和事件驱动的应用模式。稳健:Dart的健全空安全类型系统可以在开发过程中就捕捉到错误。整个平台拥有极好的可扩展性和可靠性,已经被大量且多样的应用在累计超过十年的生产环境中实战检验过,其中包括Google的一些关键业务应用,如GoogleAds和GoogleAssistant等。健全空安全增强了类型系统的稳健性,同时提高了性能。借助DartFFI,您可以获得更强的可移植性,同时沿用由C语言编写的既有代码,在处理对性能要求极为严苛的任务时,可以尽情使用经过精心优化的C语言代码。
健全的空安全
自Dart2.0中引入健全类型系统以来,Dart语言中最重大的新增内容便是健全空安全。空安全进一步增强了类型系统,让您能够捕捉到空值错误,此类错误经常导致应用崩溃。启用空安全后,您就可以在开发过程中捕捉到空值错误,避免应用在生产环境中发生崩溃。
健全空安全的设计围绕一套核心原则展开。您可以阅读官方文档了解这些原则对开发者的影响。
默认不可空:从根本改变类型系统
在空安全出现之前,开发者面临的核心挑战在于无法区分预期收到空值的代码和不接受空值的代码。几个月前,我们在Flutter的master渠道中发现了一个错误,多个flutter工具命令在特定计算机配置下会发生崩溃,并触发空值错误:
Themethod=wascalledonnull
。问题出自如下代码:
finalintmajor=version?.major;finalintminor=version?.minor;if(globals.platform.isMacOS){//pluginpathofAndroidStudiochangedafterversion4.1.if(major=4minor=1){...
您发现错误了吗?由于version可能为空,所以major和minor也可能为空。如果单独检查此处代码,这一错误似乎并不难发现。但实际上,即使经过了严格的代码审查过程(如Flutterrepo所采用的代码审查流程),也总是难免有这样的漏网之鱼。在启用空安全后,静态分析能够立即捕捉到这一问题(如下图)。您可以在DartPad中亲自上手体验。
△IDE中的分析结果这只是一个非常简单的错误。我们早期在Google内部的代码中使用空安全时,捕捉到的复杂错误远多于此。其中一些是多年前就已经发现的bug,但在通过空安全进行额外的静态检查前,很多团队都未能找到原因。
内部团队发现,他们经常检查表达式中是否存在空值,而这些表达式永远不可能为空。这个问题在使用protobuf的代码中最常见,其中可选字段在未经设置时会返回一个默认值,而且永不为空。这会导致代码混淆默认值和空值,并错误地检查默认条件。GooglePay团队在他们的Flutter代码中发现了一些bug,在尝试访问Widget上下文之外的FlutterState对象时会出错。在采用空安全之前,这些对象会返回null并掩盖错误;在采用空安全之后,健全分析确定这些属性永远不可能为空,并会给出分析错误。Flutter团队发现了一个bug:如果在Window.render()中向scene参数传递空值,则Flutter引擎可能会崩溃。在向空安全迁移的过程中,他们添加了一个提示,将Scene标记为不可空,即可轻松防止空值可能引发的应用崩溃。在默认不可空的前提下工作
启用空安全后,声明变量的基础方法会发生变化,因为默认类型不可为空:
//在空安全的Dart中,以下均不可为空vari=42;//Inferredtobeanint.Stringname=getFileName();finalb=Foo();
如果您想要创建可能同时包含值或null的变量,则需要在声明变量时在类型后面显式添加?后缀:
//aNullableInt可以为整型或nullint?aNullableInt=null;
空安全的实现很稳健,并提供丰富的静态流程分析,方便开发者轻松处理可空类型。例如,局部变量在进行空值检查后,Dart会将其类型从可空提升为非空:
intdefinitelyInt(int?aNullableInt){if(aNullableInt==null){return0;}//aNullableInt现在会被提示为非空intreturnaNullableInt;}
我们还添加了一个新的关键字,required。当一个命名的参数被标记为required(在FlutterwidgetAPI中经常出现),而调用者忘记提供该参数时,就会发生如下分析错误:
渐进迁移至空安全
空安全对于我们的类型系统而言是一项根本性的改变,因此如果我们执意强制所有开发者采用,势必会造成严重的混乱。因此,我们想让您自行决定合适的迁移时机,空安全将是一项可选特性:在做好准备之前,您可以在无需强制启用空安全的情况下使用Dart2.12。您甚至可以在尚未启用空安全的应用或package中依赖已启用空安全的package。
为了帮助您将现有代码迁移至空安全,我们提供了迁移工具和迁移指南。该工具会首先分析您所有的代码,然后您可以交互式地查看工具推断出的可空属性,如果您不同意工具得出的结论,则可以添加可空性提示以更改推断。添加迁移提示可能会大幅提升迁移质量。
目前,在默认情况下,使用dartcreate和fluttercreate新创建的package和应用中不会启用健全空安全。在大部分生态系统完成迁移后,我们预计将在后续的稳定版本中默认启用。您可以通过dartmigrate在新创建的package或应用中轻松启用空安全。
Dart生态系统的空安全迁移状态
去年,我们提供了健全空安全的数个预览版和Beta版,旨在为生态系统提供首批支持空安全的package。这项工作非常重要,我们建议大家有序迁移至健全空安全,也就是说,在所有依赖项迁移完成之前,最好不要迁移自己的package或应用。
我们已发布由Dart、Flutter、Firebase和Material团队所提供的数百个package的空安全版本。令人惊喜的是,Dart和Flutter生态系统对此也予以巨大的支持,pub.dev现在共有1,多个package支持空安全。而且重要的是,最受欢迎的package已率先完成迁移,截止到Dart2.12发布时,前个最受欢迎的package中已有98个支持空安全,而在前和前的package中,支持空安全的比例则为78%和57%。我们希望在接下来的几周,pub.dev上能够出现更多支持空安全的package。我们的分析表明,pub.dev上的绝大多数package已经可以开始迁移。
充分健全的空安全的优势
完成迁移后,您的项目就处于健全的空安全模式下了。这意味着Dart能够完全确保具有不可空类型的表达式不为空。当Dart分析完您的代码并确定某个变量不可为空时,该变量将始终不可为空。Dart与Swift都拥有健全的空安全,但有些编程语言在这方面仍有待改进。
Dart的健全空安全还暗含另一项备受期待的优势:您的程序可以更小、更快。由于Dart能够确保不可为空的变量绝不为空,因此可以实现优化。例如,Dart的运行前(ahead-of-time,AOT)编译器可以生成更小更快的原生代码,因为当其知道变量不为空时,便不再需要添加空值检查了。
DartFFI:集成Dart与C语言代码库
您可以通过DartFFI调用C语言编写的既有代码库,从而增强可移植性,还可以通过精心打磨的C代码完成对性能要求极为严苛的任务。从Dart2.12起,DartFFI已结束Beta测试阶段,现已进入稳定状态,可以用于生产环境。我们还添加了一些新功能,包括嵌套结构和按值传递结构。
按值传递结构
在C语言中,结构可通过引用和值进行传递。FFI以前仅支持按引用传递结构,但从Dart2.12开始,也支持按值传递。下方的简单示例中,两个C函数使用引用和值完成传递:
structLink{doublevalue;Link*next;};voidMoveByReference(Link*link){link-value=link-value+10.0;}CoordMoveByValue(Linklink){link.value=link.value+10.0;returnlink;}
嵌套结构
CAPI通常使用嵌套结构,这种结构本身也包含结构,比如以下示例:
structWheel{intspokes;};structBike{structWheelfront;structWheelrear;intbuildYear;};
从Dart2.12起,FFI将支持嵌套结构。
API改动
作为FFI稳定版发布内容的一部分,并且为了支持上述功能,我们做了一些小幅的API改动。
现在不允许创建空结构(重要改动参照#),并会给出弃用警告。您可以使用一个新的类型Opaque来表示空结构。dart:ffi函数sizeOf、elementAt和ref现在需要编译时的类型参数(重要改动参照#)。因为在package:ffi中增加了新的便利函数,所以在常见的情况下,无需额外添加关于分配和释放内存的模板代码:
//分配一个Utf8数组,使用Dart字符串填充,然后传递给C方法并转换结果,最后释放arg////API变更前:finalpointer=allocateInt8(count:10);free(pointer);finalarg=Utf8.toUtf8(Michael);varresult=helloWorldInC(arg);print(Utf8.fromUtf8(result);free(arg);//API变更后:finalpointer=callocInt8(10);calloc.free(pointer);finalarg=Michael.toNativeUtf8();varresult=helloWorldInC(arg);print(result.toDartString);calloc.free(arg);
自动生成FFI绑定
对于大型的API接口,编写与C代码集成的Dart绑定极其耗时。为减轻这一负担,我们为大家准备了绑定生成器,可以通过C头文件自动创建FFI封装代码,欢迎试用。
FFI路线图
核心FFI平台完成后,我们的工作重心将转向基于核心平台扩展FFI功能集。我们正在研究的一些功能包括:
ABI特定数据类型,如int、long、size_t(#)结构中的内联数组(#)Packed结构(#)联合类型(#)对Dart开放终结方法(finalizer)(#,请注意,您现在可以通过C语言使用终结方法)FFI使用示例
在过去的几个月中,我们看到大家在使用DartFFI集成一系列基于C语言的API时,发掘出了许多有创意的用法。下面介绍几个示例:
open_file是一个用于在多个平台打开文件的API,使用FFI在Windows、macOS和Linux上调用操作系统原生API。