🌆Dart学习简记
type
status
date
slug
summary
tags
category
icon
password
Status
毕设选题是设计一个 App,因此查阅学习了一下目前流行的移动端软件实现方案。其中较为流行的 Flutter 框架,底层语言使用了 Dart,其作为 Google 开源的语言之一,具备良好的语言设计。
Dart 是一门由 Google 开发并被批准为 ECMA 标准(ECMA-408)的通用编程语言,可以用于构建 Web 端、服务端和移动端应用程序。它是开源的,使用宽松的免费软件许可证(修改版BSD 许可证)。Dart 是完全面向对象的,使用类和单继承,其并发基于 Actor 模型,可编译为 JavaScript,语法风格是类 C 的。它支持接口、mixin、抽象类、泛型和可选类型。
跨平台移动端开发工具 Flutter 正是基于 Dart,目前 Flutter 已经作为移动端考虑跨端开发和性能的首选。
Dart 的类型被设计为渐进类型,并且其类型系是非严格的,这意味着 Dart 类型检查器在编译时不会标记某些类型错误,反而依靠运行时检查来确保类型安全。Dart 类型非严格的主要来源是协变泛型。
Dart 没有接口、抽象基类或“普通”类。相反,Dart 只有作为接口的类,你可以实现它们,或者通过继承它们来作为基类使用,或者通过 mixin 来重用它们的实现。Dart 中的每个类型都是一个对象,所以基础类型(如数字)和常规对象类型是没有区别的,但是定义顶层函数和变量是可以的,所以顶层类中不再需要 public static void 咒语。
Dart 支持用户自定义算术运算符,但不支持基于类型的方法重载,因为 Dart 中的所有函数都是虚拟的,所以方法重载是没有意义的。
null 感知运算符和级联为点操作符提供了更简洁的语言。
因为所有类型都是可选的,Dart 本质上是一种动态语言。但是你很少有机会产生疑问,Dart 中有 null 但没有 undefined,因此只有 == 但没有 ===,只有 true 是 true,所以也不需要使用
foo&& foo.bar()
来检查 null。Dart 有常见的整数和浮点数字类型,但是+和==不会产生运行时类型转换,Dart 中异步的使用方式是通过以下四种模式:ㅤ | One | Many |
sync | {...return e;...} | sync* {...yield e;...for()...} |
async | async {...await e;...} | async* {...await e ... yield e;;...for()...} async* {...await e ... yield e;;...for()...} |
也就是说,Dart 通过使用
sync*
块中的生成器和 for
循环,对同步数据流 Iterable<T>
的生成和使用提供良好的支持,使用 async
块中的 await
表达式,生成和使用 future(Future<T>
),最后且最重要的是,使用 async*
块中的异步生成器和 for
循环生成及使用异步数据流Stream<T>
。内置对异步编程的支持在任何现代编程语言中都是必不可少的,虽然数据存在于内存中,但让它们在网络上传输,那是非常“遥远”的,在如此高延迟下进行同步访问的代价极高。类似于 JavaScript,但又不同于其他支持生成器的语言,Dart 拥有所谓的委派生成器,避免流的生成在嵌套和递归中呈现二次方规模的爆发性增长。Dart 中的反射实现历史是曲折的,由于 Dart 的目标平台的限制,使得反射的支持十分昂贵,不得不考虑其对代码体积带来的影响。
和 Web 端类似的,移动应用也面临着自身的挑战。相比传统应用,它们必须注意节省电能,这使得提升性能方面又多了一个新的研究方向。移动设备的网络连接可能速度不快,需要付费甚至会发生断线。移动设备大小的限制,往往会给移动应用强加一个特殊的生命周期。
Dart 立志成为一个精心设计的开发平台,为当下的开发者要编写的各种应用提供支持。它力图屏蔽底层平台的各种问题与实现的细节,让开发者能方便地使用这些新兴平台所提供的强大功能。
设计原则
一切皆对象
Dart 运行时所处理的值都是对象,包括数字、布尔值等基础数据。Dart 屏蔽了底层细节,使开发者专心面对要解决的实际问题,举个例子,集合类能够操作任意类型的数据,使用者无须关注自动转箱、拆箱的问题。
面向接口,而非面向实现
- Dart 的类型基于接口,而不是类。作为一项原则,任意类都隐含了一个接口,能够被其他类实现,不管其他类是否使用了同样的底层实现(部分 core type 例外比如数字、布尔值与字符串)。
- Dart 没有 final 方法,允许重写几乎所有方法(同样,部分内置的操作符例外)。Dart 把对象进行了抽象封装,确保所有外部操作都通过存取方法来改变对象的状态。
- Dart 的构造函数允许对对象进行缓存,或者从子类型创建实例,因此使用构造函数并不意味着绑定了一个具体的实现。
类型可选
关于静态类型检查的争议很多,支持者认为静态类型信息实现了程序自文档化,使代码易读,此外类型信息还简化了各种分析任务,有助于编译器提升程序性能。但是苛刻的类型系统贯穿开发始末,会极大限制开发的流程,并且先进的类型系统很难理解和使用。
在 Dart 中:
- 类型在语法层面上是可选的
- 类型对运行时语义没有影响
因此完全可以把 Dart 当作一门动态语言来使用,如果需要,也可以使用类型注解。对于可能存在的类型不一致和遗漏,Dart 会给出警告,不会报错。同时,Dart 编译器不会拒绝一段缺少类型或类型不一致的程序。因此,使用类型不会限制开发者的工作流程。类型缺失或不完整的代码,仍然可被用来测试和实验。
数据类型
Dart 中的数据类型包括 int、double、num、bool、String、Symbol、List、Map 等等。
Dart 的标识符唯一特别的就是不允许标识符中出现非 ASCII 字母。
数字
Dart 中的整数具体存储形式受到目标平台的影响。如果是编译成 JavaScript,那么 Dart 中的 int 和 double 都将转为 JavaScript 中的 Number 值(64 位双精度浮点数)。也就是说,整数将最多只有 53 位的精度(因为 IEEE754),并且在 web 上,int 也实现了 double。
一般来说,native(non-web) Dart 中的整数需要以 64 位二进制补码存储,如果能带来某些优势,VM 可能会将它们存储为位数更小的数字,但具体的细节是开发者不可见的。
在 64 位 VM 中,所有指针都是 64 位的,所以不能直接在堆中存储比这更小的数字(除开 list)。不过如果是有符号 63 位整数,可以存储在指针中(称之为 Smi,即“small integer”),这样就不需要分配堆内存来存 64 位数字对象了。并且 Dart 仍然能区别数字和指针,这对垃圾回收器来说是很重要的。在 32 位 VM 中,指针是 32 位的,所以 Smi 是 31 位的,否则存储在堆中。
对于函数中的局部变量( VM 知道其被访问的方式),VM 可以选择拆箱它的值,将普通整数存储在栈上,而不是堆对象或者 Smi。如果编译器知道你只使用了 16 位整数,它理论上只会使用 16 位。实际上,任何小于 32 位的存储都可能导致效率低下。
这些优化是出于性能原因考虑,编译器试图避免不必要的开销,目前的实现是这样,但是未来可能会有所改变,但对开发者不可见。
对于大整数,Dart 同样有 BigInt 类型。
布尔值
不像其他语言,我们不能将任意表达式强制转换为布尔值。如果你期望 if 语句能将非 null 值转换为 true,那你将会失望。Dart 不支持内置的强制转换。
我们需要自己判空:
字符串
单引号、双引号、多行字符串
"""str"""
在 Dart 一样支持。相邻的字符串会隐式地连接起来作为单行字符串。Dart 还支持原始字符串。原始字符串不支持转义序列,它们包含的内容完全等同于引号间的内容。此外还支持插值:
Symbol
Symbol 用来表示程序中声明的名称, 使用#开头:
List & Map
即列表和键值对,在其字面量前加 const 修饰符能够变为编译时常量。
流程
Dart 支持条件运算符和 if 条件判断,支持 for、while、do 循环,以及更高级的 for-in 循环(可迭代对象)。Dart 中的 switch 语句没有穿透,仍然需要加 break。此外 case 都必须是编译时常量。
除了 try-catch,Dart 还支持 rethrow,将本地不需要处理的错误传递到调用链上层。
assert 在生产模式是不会有任何影响的。
类
Dart 中创建类的 new 关键字是可选的,下面的例子实现了一个 Point 类:
Point 的+运算符实际上是一个有怪异名称和调用语法的实例方法。
除了内置的 Object 类,每个类都有一个父类,如果没有显式指定父类,那么默认父类是 Object。
Dart 的 assessor 是为方便访问值所提供的特殊方法,事实上就是 JavaScript 的 getter 和 setter。例如将 Point 类改为极坐标的形式,但仍然支持直角坐标。
事实上声明实例和静态变量都会自动引入一个 getter,如果变量是可变的,则一个 setter 也会被自动引入。任何外部对类字段的引用都需要经过 assessor,除非是类内部对其的使用。这意味着类的底层实现随时都可以改动,而不需要客户端修改代码,甚至重新编译,这被称为“表征独立”。
除了实例变量,类还有类变量即静态变量(加一个
static
),即使类没有一个实例,类变量也是存在的。类变量是延迟初始化的,在其 getter 第一次被调用时才会初始化。final 变量表示变量初始化之后就无法改变,final 字段有 getter 但是没有 setter,它必须在任何实例方法运行前进行初始化(常见的如立即初始化以及构造函数 this 形参初始化)。
试图给一个 final 实例变量赋值通常会导致一个名为 NoSuchMethodError 的错误,因为赋值操作只是调用 setter 的语法糖,而 final 实例变量所对应的 setter 方法是未定义的。单独声明个对应的 setter 是可行的,它也会被调用。然而这对实例变量的值没有任何影响,在 final 变量初始化之后,它的值就无法改变了。
试图用方法重写父类中的 getter 或 setter,将会导致报错,反之亦然。对于方法重写,重写方法的参数个数不一致也会导致报错。
判断 Dart 中类创建的两个实例对象是否相等可以使用 dart:core 库中定义的 identical 方法。
构造函数
如果类没有明确构造函数,那么一个隐式的构造函数将被创建,它没有参数和函数体。
Dart 同样有初始化列表,这可以解决 final 变量没有 setter 同时又需要构造函数参数来计算的情况。对于子类,需要通过
super
调用父类的构造函数:注意,如果初始化列表中没有调用父构造函数,那么一个隐含的父构造函数
super()
将会被添加到初始化列表的尾部。这就是为什么 Object 类不需要子类调用 super()
的原因。Dart 中还有重定向构造函数,可以在旧构造函数之上指定新的构造实现:
如果想重利用已经分配的实例。Dart 提供工厂构造函数,使开发者能够利用缓存来返回对象。
factory
关键字用来标识工厂构造函数,该工厂函数必须有一个返回一个对象的函数体。工厂构造函数可以从缓存中返回对象,或选择分配一个新的实例。它甚至可以创建一个不同类的实例(或者从缓存或其他数据结构中查找它们)。只要生成的对象符合当前类的接口,则一切都会按预期执行。有些对象是可以在编译时计算的常量。Dart 提供了 const 关键字来标识这些常量。const 对象是不可变的,它们的值在编译时就已经确定了。const 对象的构造函数必须是 const 的,而且所有的实例变量都必须是 final
定义的常量,以来保证不可变性。此外,一个常量构造函数不能有函数体。它可以有一个初始化列表,前提是只计算常量(假设参数是已知的常量)。
我们仍然可以使用 new 调用常量构造函数,这样做的话,则传递的参数不再受限制,但结果不再是常量。
常量的值可以提前计算,只需一次,无须重新计算。Dart 程序中的常量是规范化的,一个给定的值只会产生一份常量。
继承
Dart 中的抽象类和其他语言一样,可以提供接口的功能,同时它可以有默认的实现。注意抽象类不能被实例化,这将导致报错,毕竟它缺少部分实现。就运行时而言,抽象方法根本不存在。毕竟,它们没有实现,也无法运行。调用抽象方法就与调用一个不存在的方法一样。
针对抽象类的使用,使用
extends
关键字,子类必须实现父类中未提供默认实现的抽象部分,否则会导致报错。使用 implements
关键字,则子类必须实现父类的所有抽象内容,包括有默认实现的部分,否则会导致报错。implements
的目的是在接口间建立预期的关联,而不是共享实现。检查一个类是否实现某个接口,可以使用
is
关键字,is
并不检查对象是否为某个类或其子类的实例,只检查对象的类是否明确实现了某个接口(直接或间接)。这和 Typescript 很像,是一种结构化类型的行为。接口的继承类似于类。类的隐含接口会继承父类的隐含接口,同时会继承父类实现的接口。同类一样,接口可以重写父接口的实例方法;另外,某些重写可能是非法的,例如重写方法与被重写方法的参数不一致,或者试图用普通方法重写 getter 或 setter,反之亦然。另外,一个接口有多个父接口,不同的父接口之间可能会产生冲突。假设一个同名的方法在多个父接口中出现,而且它们的参数不一致,则在这种情况下,互相冲突的方法没有一个会被继承。如果一个父类定义了一个 getter,而另一个父类也定义了同名的普通方法那么结果也是一样的。
Dart 中的计算都是围绕对象方法的调用。如果调用了一个不存在的方法,则默认的行为是抛出 NoSuchMethodError 错误。事实上,当调用一个实例中不存在的方法时,Dart 运行时会调用当前对象的
noSuchMethod()
方法。因为 Object 类的 noSuchMethod()
方法的实现就是抛出 NoSuchMethodError 错误,所以我们通常都会看到这个熟悉的行为。这个方案的优点在于
noSuchMethod()
能够被重写。例如,如果你要实现另一个对象的代理,那么你可以定义代理的 noSuchMethod()
方法,并把所有的调用都转发给代理的目标:noSuchMethod()
的参数是 Invocation
类的一个实例,它是定义在核心库中的一种特殊类型,用于描述方法的调用。一个 Invocation
反映了原始的调用,描述了我们试图调用方法的名称、传递的参数及其他一些细节。为了真正把每个调用转发给 forwardee,我们在
noSuchMethod()
的实现中使用了一个辅助函数
runMethod()
,它接收一个接收对象和一个 invocation,并使用提供的参数调用接收对象上对应的方法。健壮的代理实现比上面的代码要复杂一些的。一个细微之处是 Proxy 不会转发在 Object 类中定义的方法,因为这些方法是继承的,并不会导致 noSuchMethod()
被调用。Object 的接口被设计得很小,可以手动拦截处理。类变量和类方法永远不会继承,所以声明一个抽象的类方法是没有意义的。
每个对象都是一个类的实例,既然一切都是对象,那么类也是对象,既然类是对象,那它们本身也是一个类的实例。类的类通常被称为元类。Dart 指定类的类型为 Type,而 Type 的元类还是 Type。对于字面量,其
runtimeType
是字面量的类型。如对于整数,其 runtimeType 是 int,而 int 的元类为 Type。你不能定义 int 的子类,也不能用另一个类实现 int。Dart 以牺牲可以继承或实现任意类来实现高性能。同理 double、num、bool、String 都不能被我们的代码继承或实现。
反射是唯一可靠的发现对象所属类的方式。对象都支持一个名为
runtimeType
的 getter,它默认返回对象所属的所属类。但是子类可以随意重写 runtimeType。可以重写 runtimeType 是非常重要的,在 Proxy 的情境中,代理对象与真实对象必须是难以分辨的。如果 runtimeType 没有被重写,那么我们可以发现,这些代理对象都是 Proxy 的实例。要注意的是,对于类来获取其元类,要使用
(Type).runtimeType
,而不是 Type.runtimeType
,后者被认为是调用类的类方法,而抛出 noSuchMethodError。Object 与其方法
Object 类的接口是很小的,大致如下:
除了操作符方法
==
的实现,以外所有方法都由 Dart 底层实现,并不直接使用 Dart 代码实现。它们被标记为 external
,表明它们的实现在其他地方。external
机制声明代码的实现来自外部。这些外部代码可以有多种提供方式:通过作为底层实现基础的外部函数接口(这里就是这样),或者甚至可能动态地生成实现。Mixin
Dart 中的类是单继承的,但是可以通过 mixin 来实现多继承的效果。每个 Dart 类都有一个 mixin。以把 mixin 看作一个函数,它接收一个父类 S 并返回一个新的拥有特定主体的 S 子类。
每次用某个特定的类调用这个函数,都会产生一个新的子类。因此,我们把 mixin 应用看作用一个 mixin 与一个父类来派生一个新类。我们把 M 与父类 S 的 mixin 操作写成
S with M
。显然,S
必须指定一个类,但是我们如何指定 mixin?通过指定一个类,每个类都通过它的主体隐含定义了一个 mixin,我们就是使用它来对 S 执行 mixin 操作。如上,CompoundWidget 的父类是 Widget 与 Collection,即类 Collection 与父类 Widget 在 mixin 之后产生的一个新的匿名类。作为 mixin 的类不能有显式声明的构造函数,不过对 Mixin
的一些限制正在被移除,具体的情况需要参见 proposed mixin specification。
库
Dart
程序是由被称为库的模块化单元组成的。通常来说,一个库由多个顶层声明组成,包括类、枚举、类型别名、变量、常量、函数和方法。库是 Dart 的基础封装单元。以_开头的成员都是库私有的。
顶层变量引入了隐含的 accessor,用户的代码不会直接访问变量。顶层变量是延迟初始化的,与类变量一样,它们的 getter 第一次被调用时才执行初始化。顶层变量和类变量一起被称为静态变量。它们的区别在于作用域,即在什么范围内能够通过名称对它们进行访问。类变量的作用域被限制在声明它们的类中(甚至子类也无法访问它们),顶层变量(也被称为库变量)的作用域覆盖了声明它们的整个库。
与类变量一样,顶层变量也可以声明为 final,在这种情况下,它们没有定义 setter 且必须在声明时就初始化。也可以把静态变量(可以是类或库变量)声明为常量,那样的话它们只能被赋予一个编译时常量且自身被视为不可变。
顶层函数(常被称为库方法)的作用域规则与顶层变量一样,在整个库中都是可用的它可以是普通函数、getter 和 setter。
在 Dart 中关声明都是顶层的,因为 Dart 不支持嵌套类。
导入库的方式是使用
import
指令,并且可以使用 as
起别名,show
和 hide
作为命名空间组合器限制导入的内容。show
和 hide
可以同时使用,但是 show
的优先级更高。库的路径可以是相对路径和绝对路径,以
dart:
开头的路径是 Dart SDK 中的库,以 package:
开头的路径是 Dart 包管理器 pub 中的库。有时,一个库可能太大,不能方便地保存在一个文件中。Dart 允许你把库拆分成较小的被称为 part
的组件,并且库其自身的私有状态在各组件间共享。每个子组件都存放在各自的文件中,而库通过使用
part 指令来引用它们。每个 part 指令都给定了一个指向对应 part 所在位置的 URI。这些 URI 与导入语句遵循同样的规则。
所有 part 都共享同一个作用域,即引用它们的库的内部命名空间,而且包含所有导入。part 的头部使用库名称来指明它所在的库。不是所有的库都有名称,但如果使用 part 来构建库,那么库本身必须要命名。各个 part 应该有很好的结构,并且按照逻辑分组,而不是纯粹地堆积代码。
对于一个 part 来说,它不能是一个库,就不能有自己的库声明,否则会导致编译错误。另外一个
part 不能属于多个库,否则也会导致错误。
export
指令允许一个库使用来自其他命名空间的对象来扩充自己的导出命名空间。如下:有时,我们需要推迟库的加载。一般情况下,我们只是为了使应用快速启动且保持初始下载量尽可能小;另一种情况可能是在拥有诸多功能的大型应用中,因为某些特性不被所有用户使用,所以实现相应功能的库也不总是需要的。不加载不会使用到的库有助于减少内存的使用。
需要延迟加载库,就在导入该库时使用
deferred
关键字,并在使用的地方采用 loadLibrary()
方法来完成库加载,该方法是异步的,返回得到结果是一个 future。函数
Dart 中的函数允许嵌套,另外 Dart 支持 lambda,因此一个 max 函数可以简写成以下形式:
可选参数必须排列在一起放置在参数列表尾部并用方括号包裹。任意必填参数都必须出现在可选参数前面。可选参数可以指定默认值但必须是编译时常量,若没有指定则为 null。
参数可以是位置参数或命名参数,命名参数要在位置参数之后声明并用大括号包裹。下面展示了这两种参数混用的情况:
命名参数始终是可选的。换而言之,对参数的分类如必填或可选,与参数的位置或命名的分类是没有关联的。你不能混合使用可选位置参数与命名参数,你只能使用其中一种。
构造函数包括生产构造函数与工厂构造函数,它们的区别在于,生产构造函数始终返回一个新的实例或者抛出一个异常。所以就算是没有显式地使用 return 语句,生产构造函数也不会返回 null。
赋值
可能并不明显,但 Dart 中的赋值通常都是函数调用,因为对字段的赋值只是 setter 方洗的语法糖而已。
像 v=e 这样的赋值操作,其确切含义取决于 v 的声明。如果 v 是局部变量或参数那么这就只是一个传统的赋值。否则,这个赋值只是对调用名为 v 的 setter 的语法糖而已
赋值是否有效取决于 setter v 是否被定义,或变量 v 是否为 final,final 变量不能重复赋值且不会导致对应的 setter 被执行。
Function 类
Function 是代表所有函数的公共顶层接口的抽象类。Function 没有声明任何实例方法。然而它声明了类方法
apply()
,此方法接收一个函数和一个参数列表,并使用提供的参数列表去调用传入的函数。apply()
的签名是:你会注意到,
apply()
的形式参数是带有类型注解的。它需要一个被调用的函数和一个位置参数的列表(可能为空)。命名参数可以通过一个名称与实际参数组成的 map 来提供。且实际参数可以是任意类型的对象。最后一个参数是可选的。大部分函数不需要任何命名参数,所以只在需要时才提供它们是比较方便的。apply()
方法提供了一种使用动态确定的参数列表来调用函数的机制。通过它,我们可以处理在编译时参数列表数量不确定的情况。模拟函数
如前面提到的,面向对象编程的关键原则是对象的行为而不是对象的实现。理想情况下,任意对象都应该能模仿其他对象。例如,Proxy 的实例被设计为模仿任意对象的行为。而函数是对象,我们应该也能够模仿它们的行为。我们怎么用 Proxy 来模仿函数呢?函数最常见和重要的行为是被调用时所执行的操作,但函数调用是一个内置的操作。我们希望能够这样写:
上面的代码能够正常工作。原来函数的执行会转换成调用一个名为
call()
的特殊方法。所有真正的函数都隐含支持一个签名跟函数本身一致的 call()
方法,它的作用是执行当前的函数。在上面的例子中,p(1)
实际上是 p.call(1)
。当然,我们不能把 p.call(1)
看作 p.call.call(1)
,那将造成无限递归。因为 Proxy 没有 call 方法,所以 noSuchMethod()
被执行,将调用发送到代理目标。此代理目标是一个函数,它有自己的 call()
方法。任何声明了 call()
方法的类都被认为隐含实现了 Function 类。注意 Function 没有声明 call()
方法。原因是没有特定的函数签名来声明
call()
可以有不同个数的参数,而且可能会有或者没有拥有不同默认值的可选参数(位置参数或命名参数)。所以,Function 真的没有通用的 call()
可以声明。也因为这样,Dart 在语言层面对 call()
方法进行了特殊处理。生成器
Dart
支持生成器,它是用来产生集合值的函数。生成器可以是同步或异步的。同步的生成器为迭代器生成提供语法糖,而异步的生成器则为流的生成提供语法糖。
迭代器与可迭代对象
迭代器是允许对集合内容按顺序进行迭代的对象。在我们想简单生成集合内容时,迭代器特别方便。支持通过迭代器进行迭代的集合被称为可迭代对象,可迭代对象必须有一个名为 iterator 的用于返回迭代器的 getter。
for-in 循环可以操作任意可迭代对象。迭代器与可迭代对象的接口分别被类 Iterator 和 Iterable 实现。
通过同步生成器,可以省去即使是实现最简单的迭代器都需要定义两个类的的麻烦。通过给函数体使用
sync*
修饰符来定义生成器函数。被调用时,此函数将立返即回一个可迭代对象 i,该对象又包含了迭代器 j。在迭代器的
moveNext()
第一次被调用时,此函数才开始执行。在进入循环后,yield
语句被执行,导致 k 被加 1,而上一次 k 的值被追加到 i 同时 naturalsTo()
的执行将暂停。在下一次 moveNext()
被调用时,暂停 yield
的 naturalsTo()
将继续执行同时循环将重复。需要牢记一点,生成器函数的函数体是在函数返回一个结果给调用者之后才开始执行的。那我们自然会问,生成器内 return 演的是什么角色?答案就是 return 会直接终止生成器。
在生成器中我们不能使用 return 语句来返回值,这样的语句将被编译器标记为错误。允许这样的语句是没有任何意义的,因为调用者已经获得了返回值,且调用者已完成处理并从调用堆栈上消失。
当
yield
暂停执行函数体时,moveNext()
返回 true 给调用者。当生成器终止时,moveNext()
返回 false。此外还有
yield-each
,语法是 yield*
后跟一个集合(可迭代对象)。yield*
将集合的所有都追加到生成器的结果中。类型
Dart 不支持基于类型的重载,原因是不同类型的默认值是不同的而 Dart 坚持类型注解不影响语义。
Dart 支持泛型。给泛型类提供类型参数并不是必需的。如果我们选择使用泛型类且不提供类型参数,则类型 dynamic 将会被隐性使用,代替所有缺失的类型参数。
针对泛型协变,在下面的例子中,
printColors
是可行且运行良好的,Apple 和 Orange 都可以作为 Fruit 的子类型,但是协变子类型可能会导致实际执行的类型不兼容,而 Dart 的类型系统针对这种情况是非可靠的。反射
我们知道反射是一种计算机处理方式,是程序可以访问、检测和修改它本身状态或行为的一种能力。换一个角度说,反射可以细分为自省——程序在运行时决定自身结构的能力,以及自我修正——程序在运行时改变自身的能力。Dart 的反射基于 mirror 概念,它指的是反映其他对象的对象,并且目前只支持自省,不支持自我修改。
Mirror
控制台输出为
分类
在官方 API 页面可以看到所有的 Mirror 类型:dart:mirrors library。Mirror 的主要类型如下
- ClassMirror:Dart 类的反射类型
- InstanceMirror:Dart 实例的反射类型
- ClosureMirror: 闭包的反射类型
- DeclarationMirror:类属性的反射类型
- IsolateMirror:Isolate 的反射类型
- MethodMirror:Dart 方法(包括函数、构造函数、getter/setter 函数)的反射类型
通过 dart:mirrors 包内顶层函数
reflecClass
获得类的“镜像”的实例,该实例的 instanceMembers
属性如下:由控制台输出结果可以看到,对于普通字段(属性),除自身外还列出了以“=”结尾的 setter 字段,对于不提供 setter 的 final 字段则只出现一次。
使用 staticMembers 将列出所有的静态字段:
输出如下:
可以发现父类静态成员没有出现在列表中,这是因为静态属性不会被继承、不能被 ChildClass 调用。
Symbol
Symbol 表示使用 Dart 的 mirror API 反射得到的实例类型,位于 dart:core 包:
源码
通过 ClassMirror 的源码,可以大概看出 Dart 语言关于反射的设计思想以及对外提供的 API:
影响
在 Java 中,当开发者多次(10w 次以上)访问、修改某一属性时,使用反射的成本会比正常访问高很多,同时会让
private
修饰符失去作用。在 Dart 中,反射的影响主要在于,编译器使用 tree shaking 的过程确定应用真正运行时使用的代码,以减少程序的大小。但是使用反射将使 tree shaking 失效,因为任何代码都有可能被使用,由此严重影响应用的启动时间和内存占用。解决 👆 问题的有效方法是,通过代码生成执行反射。为了“告知”编译器使用反射的代码和方式,开发者可以使用
dart:reflectable
库,通过特定元数据注解反射代码。另一个影响在于最小化,其表示对下载到 Web 浏览器的源程序进行压缩的过程。在最小化过程中,源代码使用的名称在编译代码中被压缩成了短名称。这一过程会对反射带来不良影响,因为最小化之后,原来表示声明的名称的字符串,不再对应程序中的实际名称。
为了解决这一问题, Dart 反射使用 symbol 而非字符串作为 key,symbol 会被执行最小化的程序 minifier 识别并使用与标识符同样的压缩方式。这也是上面的输出中出现 Symbol(…)的原因。开发者也可以通过 MirrorSystem 提供的 static String getName(Symbol symbol)方法获得非最小化名称字符串。
小结
目前来看 Dart 还不算一门“足够完善”的语言,比如反射机制的不完全、文档教程匮乏等等,相信随着
Flutter 的发展,这门语言的发展会更加地好。
- Twikoo