Open WangShuXian6 opened 5 years ago
Dart是一 门通用编程语言,是有着类似 C 语言的语法的新语言,其目的是让绝大多数程序员快速上手。 下面是约定俗成的 “Hello World"示例,
main() { print('hello world'); }
Dart是纯面向对象、基于类、使用可选类型、支持混入式继承及Actor模式的并发编程语言。
Dart立志成为 一 个精心设计的开发平台,为当下的开发者要编写的各种应用提供支持。 它力图屏蔽底层平台的各种问题与实现的细节,让开发者能方便地使用这些新兴平台所提供的强大功能。
Dart是一门纯面向对象的编程语言,这意味着 Dart 程序在运行时所处理的值都是对象,甚至包括数字、布尔值等基本数据,无 一 例外。 Dart坚持对所有数据统一处理,这方便了所有与语言相关的人员:语言的设计者、实现者,以及最重要的使用者。 举个例子,集合类能够操作任意类型的数据,使用者无须关注自动转箱、拆箱的问题。 类似的底层细节与开发者要解决的实际问题是无关的,编程语言的 一 个关键任务是把开发者从各种可认知的负担中解放出来。 采用统 一 的对象模型的同时,简化了系统实现者的任务
关注对象的行为而非它的内部实现,这是面向对象编程语言的核心原则
• Dart 的类型基于接口,而不是类。作为一项原则,任意类都隐含了一个接口,能够被其他类实现,不管其他类是否使用了同样的底层实现(部分core type 例外,比如数字、布尔值与字符串)。 • Dart 没有final 方法,允许重写几乎所有方法(同样,部分内置的操作符例外)。 • Dart 把对象进行了抽象封装,确保所有外部操作都通过存取方法来改变对象的状 态。 • Dart 的构造函数允许对对象进行缓存,或者从子类型创建实例,因此使用构造函数并不意味着绑定了一个具体的实现。
静态类型信息可以为用户与计算机提供富有价值的文档。如果使用得当,则这些信息能使代码更易读,特别是涉及多个库时,也使自动化工具更容易协助开发者。 类型简化了各种分析任务,特别是可以帮助编译器提升程序的性能。 它还有助千检测编程错误。
它的具体定义是: • 类型在语法层面上来说是可选的; • 类型对运行时语义没有影响。
类型变为可选,照顾了那些不愿意与类型系统打交道的开发者。 因此而选择Dart 的开发者,完全可以把Dart 当成一门动态类型语言。 虽然类型可选,但只要代码中有类型注解,就意味着代码有了额外的文档,所有的编码人员都会从中受益。 类型注解同时让工具能更好地配合开发者的开发工作。 对于可能存在的类型不一致和遗漏, Dart 会给出警告,不会报错。这些警告的程度与性质都经过校准,不会铺天盖地地出现,确保真正对开发者有益。 同时, Dart 编译器不会拒绝一段缺少类型或类型不一致的程序。因此,使用类型不会限制开发者的工作流程。类型缺失或不完整的代码,仍然可被用来测试和实验。 静态的正确性与灵活性之间的平衡使得类型服务千开发者。
Dart 必须在当下的浏览器上高效运行。 Dart 必须让当下的开发者能够快速上手, 这决定了它的语法必须是近似C 语言的,也决定了Dart 的语义选择不能偏离主流开发者的期望。
Dart 的语义已经受到了上述限制的影响时,我们应该注意为此所做的设计决策而引入的利弊权衡,包括字符串、数字的处理,以及返回语句等。
Dart 的一个简单表达式:
3
毋庸置疑,这个表达式的值是整数3 。
稍微复杂的表达式:
3 + 4 (3+4}*6 1 + 2 * 2 1234567890987654321 * 1234567890987654321
它们的值分别是7 、42 、5 及 I 524 157 877 457 704 723 228 166 437 789 971 041 。
Dart 中的整数很像数学里的数字,它们没有32 位或64 位可代表的最大值的限制,其大小的唯一限制是可用内存。
Dart 不仅支持整数,也支持浮点数、字符串、布尔值等。许多内置类型都有简便的语法:
3.14159 //一个浮点数 'a string' "another string - both double quoted and single quoted forms are supported" 'Hello World' //你已经见过它 true false// 所有的布尔值都在这里 [] //一个空的列表 (0, 1.0, false, 'a', [2, 2.0, true, \b"]] //有5 个元素的列表,最后一个也是列表
Dart 支持标准的单行注释,即在II之后的内容将被忽略,直到行末。 最后两行是用字面量表示的列表,第1 个列表为空,第2 个列表的长度是5, 且最后一个元素是另一个长度为4 的字面量列表(注:这里的列表相当千其他语言的数组,因为Dart 是用List 来表示数组的,所以还是翻译为列表)。
[1, 2, 3] [1]
上面例子的值是2 。列表的第1 个元素的索引是0 ,第2 个是1 ,以此类推。 列表有length与isEmpty 两个属性(它还有更多的属性)。
[1, 2, 3].length; // 3
某些Dart 的实现可能并不符合这一原则。在Dart 被编译为JavaScript 时,所有数字都变成了JavaScript的数字,而JavaScript 只有数字类型且没有整数类型,即要用浮点数来表示整数,并有大小限制。所以,大千2^53^ 的整数可能就不那么容易获得了。
[].length; // 0 [].isEmpty; // true ['a'].isEmpty; // false
定义函数 Dart 函数main()
main() { print ('Hello World'); }
一个Dart 程序的执行总是开始千对main()函数的调用。 每个函数都由函数头与函数体组成。 函数的头部定义了函数的名称与参数(我们的示例函数没有参数)。 main()方法的函数体中只有一条语句,即调用了接收一个参数的print()函数。 这里传递的参数是一个字面量的字符串'Hello World' 。 程序运行的效果是打印出“Hello World" 。
另一个函数:
twice(x) => x * 2;
声明了一个名为twice 的函数,并有一个名为x 的参数。该函数返回x 乘以2 的结果。可以这样写来执行它:
twice(2)
以上函数调用的结果是4 。这个twice 函数有两部分,一部分是由函数名与形式参数组成的函数签名,另一部分就是跟在
=>
后面的只包含了一个表达式的函数体。另一种更传统的书写方式是:twice(x) { return x * 2; }
以上两个例子是完全相等的,但在第2 个例子中,函数体可以包含零个或多个语句。它只是调用了一个retum 语句,使函数计算出x*2 的结果并将其返回给调用者。
另一个例子如下:
max(x, y) { if (x > y) return x; else return y; }
它将返回两个参数中更大的那一个。我们同样可以把它缩写成这样:
max(x, y) => (x > y) ? x : y;
第1 种形式使用了if 语句,这在其他编程语言中是很常见的;第2 种形式使用了条件表达式,同样,这在类C 的编程语言中也是很常见的。使用表达式能让我们使用缩写方式来定义函数。
一个更加复杂的函数:
maxElement(a) { var currentMax = a.isEmpty ? throw 'Maximal element undefined for empty array' : a[0]; for (var i = 0; i < a.length; i++) { currentMax = max(a[i], currentMax); return currentMax; } }
这个名叫maxElement 的函数,接收一个列表a 并返回其中的最大值。在这里,我们真正需要使用常规方式来定义函数,因为这里的计算有很多步骤,会产生一连串的语句
函数体的第1 行声明了一个名为currentMax 的变量,并对其进行初始化。Dart 程序中的每个变量都必须显式声明。变量currentMax 代表我们目前数组中的最大值。
在许多编程语言中,开发者可能会选择把currentMax 初始化为一个值,这个值代表可能的最小整数,它的典型名称是MIN_INT 。在数学上, “可能的最小整数”的说法是很荒谬的。但是,在数字大小有限制的编程语言里,这个说法是有意义的。前面介绍过, Dart的整数是没有大小限制的,所以我们直接把currentMax 初始化为列表的第1 个元素。如果列表为空,则我们不能那样做,那也意味着传入的参数a 是无效的,因为你无法获取一个空列表的最大值。因此,我们检测了a 是否为空。如果为空,我们就会抛出一个异常,否则我们就用列表的第1 个值来初始化currentMax 。 异常是使用throw 语句抛出的。throw 关键字后面跟着一个定义抛出内容的表达式。在Dart 中,任何类型值都可以被抛出,不要求它们是特定的Exception 类型。在这里,我们抛出了一个描述问题的字符串。在下一行的开头,循环语句for 遍历了整个列表1, 使用前面定义好的max 函数,把每个元素依次与currentMax 进行比较。如果当前元素大千currentMax, 则我们把它作为新的最大值赋给currentMax 。循环结束后,我们就可以断定currentMax 是列表中最大的元素并把它返回。
Dart 允许你在类外部定义函数(比如前面的twice 、max 与maxElement) 与变量。虽然如此, Dart 是一门纯面向对象语言。我们 前面看过的所有值,包括数字、字符串、布尔值、列表甚至函数本身都是Dart 中的对象。所有这些对象都是某个类的实例。像length 、isEmpty 这些操作,甚至[]索引操作符,都是对象的方法。
编写一个类。 Point 类,它代表了直角坐标系的点:
class Point { var x, y; Point(a, b) { x = a; y = b; } }
Point 类有两个实例变量(或者字段) x 与y。要创建一个Point 类的实例,我们可以用new 表达式来调用它的构造函数:
var origin= new Point(0, 0); var aPoint = new Point(3, 4); var anotherPoint = new Point(3, 4);
以上3 行创建了3 个新的不一样的Point 实例。特别是, aPoint 与anotherPoint 是两个不同的对象。每个对象都有唯一标识,它们通过这个标识来区分彼此。 每个Point 的实例都有各自的x 和y 变量的副本,可以通过点符号来访问它们:
origin.x // 0 origin.y // 0 aPoint.x // 3 aPoint.y // 4
变量x 与y 的值是由构造函数的实际参数来设置的,而构造函数是由new 调用的。将构造函数的形式参数直接赋值给同名的字段,这种方式非常普遍,所以Dart 为此提供了一 种特殊的语法糖:
class Point { var x, y; Point(this.x, this.y); }
新版的Point 与原版完全相同,但更简洁。我们再给它添加一些行为:
class Point { var x, y;
Point(this.x, this.y); scale(factor) => new Point(x factor, y factor); }
>这个版本多了一个scale 方法,它接收一个定义比例系数的参数factor, 并返回一个新的点。新的点的坐标是根据当前点的坐标按照factor 缩放而得来的。
```dart
aPoint.scale(2).x // 6
anotherPoint.scale(10).y // 40
另一个有趣的操作是对点进行加法操作:
class Point { var x, y; Point(this.x, this.y); scale(factor) => new Point(x * factor, y * factor); operator +(p) => new Point(x + p.x, y + p.y); }
现在我们可以写这样的表达式了:
var temp = (aPoint + anotherPoint).y; // 8
点的
+
操作符的行为就像是一个实例方法,而事实上,它们就是一个有着怪异名称和调用语法的实例方法。Dart 也支持静态成员。我们可以给Point 添加一个计算两点间距离的静态方法:
import 'dart:math';
class A { static distance(pl, p2) { var dx = pl.x - p2.x; var dy = pl.y - p2.y; return sqrt(dx dx + dy dy); } }
>static 修饰符表明此方法不针对某个特定实例,它无法访问实例变量x 和y, 因为实例变量对千每个Point 实例都是不同的。这个方法使用了一个库函数sqrt()来计算平方根。
>你可能会问, sqrt()从何而来?要理解这一点,我们需要先解释Dart 的模块化理念。
>Dart 代码用库作为模块化的基本单元。每个库都定义了一个的命名空间,这个命名空间包含所有在库中声明的实体的名称,其他库的实体也能被导入进来。Dart 核心库中声明的实体,都会被隐含地导入到所有的Dart 库中。然而, sqrt()并不在核心库中。它位于一个名为dart:math 的库中,如果你要使用它,就必须先显式地导入这个库。
>下面是一个导入的库的完整例子,它包含了Point 类:
```dart
library points;
import 'dart:math';
class Point {
var x, y;
Point(this.x, this.y);
scale(factor) => new Point(x * factor, y * factor);
operator +(p) => new Point(x + p.x, y + p.y);
static distance(pl, p2) {
var dx = pl.x - p2.x;
var dy = pl.y - p2.y;
return sqrt(dx * dx + dy * dy);
}
}
我们声明了一个名为points 的库并同时导入了dart:math 库。 这个导入的库使得points库可以访问sqrt 函数。现在,任意其他库都可以导入points 库来使用我们的Point 类。 需要注意的一个关键细节是import 后面跟着的是一个字符串
'dart:math'
。 一般情况下,导入指向的都是用字符串表示的统一资源标志符(UR1) 。编译器通过这些URI 指定的位置去寻找对应的库。 Dart 的内置标准库都使用'dart:c'这种方式的UR1 ,其中c代表某一特定库。
Dart 的设计深受早期编程语言的影响,特别是Smalltalkw 、Java 和JavaScript。Dart 的语法贴近C 语言、Java 和JavaScript。 从某些角度看,特别是在坚待纯对象模型这点上,Dart 的语义贴近Smalltalk 。
不过, Dart 有很多至关重要的不同点。Dart 引入了自己的基于库的封装模型,这不同于上面提到的所有语言。 Smalltalk 支待对字段进行基千对象的封装,方法和类则是全局的。 Java 使用基于类的封装和包级别的访问控制, JavaScript 则完全依赖闭包的封装。 与Smalltalk 和Java 相似, Dart 基于类,支持单继承,但增加了基千混入的继承,这种实现方式首次出现千Smalltalk 的方言Strongtalk中。 Smalltalk 的类方法是实例方法,而Dart 的类方法更像Java 风格的静态方法,所以,它们不完全一样。 Dart 的构造函数在语法上与Java 非常相似,实际上却有很大不同。 Dart 的类型检查也非常像Strongtalk。 Dart 的并发机制接近最早的Actor 模式(尽管势在必行),同样,这与上面提及的语言都不一样。 Erlang 的成功是促使Dart 选用Actor 模式的关键因素,但不同的是, Dart 采用了非阻塞的并发模型。 Dart 还内置了对异步编程的支待,这方面受到了C#的巨大影响。
在Dart 中,一切皆对象,这甚至包括了最简单的数据如数字或布尔值true 和false 等。 一个对象由(可能为空的)一组字段提供状态,由一组方法提供行为。 对象的状态可以是可变或不变的。对象的方法永不为空,因为所有的Dart 对象都具备一定的行为。 对象从它们的类中获得行为。每个对象都有一个类,我们将之表述为对象是类的一个实例。 因为每个对象都有一个决定其行为的类,所以Dart 是一门基于类的语言。
Point 类:
import 'dart:math';
class Point { var x, y; Point(this.x, this.y); scale(factor) => new Point(x factor, y factor); operator +(p) => new Point(x + p.x, y + p.y); static distance(pl, p2) { var dx = pl.x - p2.x; var dy = pl.y - p2.y; return sqrt(dx dx + dy dy); } }
>Point 类的实例各自都有两个字段x 和y 构成对象的状态。
>它们也拥有几个提供有用行为的方法,这些方法包括scale 和+,但实际上它们还拥有其他方法。
>这些额外的方法不是由Point 类自身定义的,而是从其父类继承而来的。
>除了内置在每个Dart 实现中的Object类,每个类都有一个父类。
>一个类可以明确地列出它的父类,但那不是必需的。如果一个类没有列出父类,那么其父类就是Object 。Point 类就是这种情况
>可以明确地指定Object 作为Point 的父类,明确指定Object 类与不指定Object 类这两种定义方式是完全相同的。
```dart
class Point extends Object {...其余定义未变}
定义对象行为的方法通常被称为对象的实例方法。注意方法distance()不属于实例行为。它是一个静态方法,不是实例方法。
accessor 是为方便访问值所提供的特殊方法。
们再次重温Point类,并考虑如何修改以让它使用极坐标的表示方式。 把字段x 和y 替换为新的字段rho 和theta 。 可能仍有部分客户端需要访问直角坐标,所以可以选择使用存储的极坐标来计算直角坐标。由此产生的类如下:
class Point {
var rho, theta;
Point(this.rho, this.theta);
x() => rho * cos(theta);
y() => rho * sin(theta);
scale(factor) => new Point(rho * factor, theta);
operator +(p) => new Point(x() + p.x(), y() + p.y());
static distance(pl, p2) {
var dx = pl.x() - p2.x();
var dy = pl.y() - p2.y();
return sqrt(dx * dx + dy * dy);
}
}
该代码实际是有错的,因为在
+
方法内使用了直角坐标来调用构造函数,但它实际要求的是极坐标。稍后会处理它。我们将忽略的另一个问题是数值精度;所有这些转换都可能得不出精确的结果。
由于已经将字段x 和y 替换为计算相应值的同名方法。所有的客户端不得不将它们对x 和y 的引用修改为对方法的调用。 例如,如果客户端有一段这样的代码:
print(myPoint.x);
则需要将它改为:
print(myPoint.x());
唯一区别就是跟在x 后面的空参数列表,这个变化虽小,但终归是一个变化。 虽然现代开发工具能帮助开发者自动重构,但在不知道有哪些客户端的情况下,修改一个使用广泛的API,迫使它们做出修改,就算是有方便的工具进行协助,也是不可接受的。
Dart 提供的getter 方法是一种更好的解决方案,它们通常被称为getter 。 getter 是一个不带参数的特殊方法,可以在不提供参数列表的情况下直接调用。 getter 方法的引入是通过在方法名前添加前缀get 。 getter 方法不需要参数列表,甚至是空的参数列表。
class Point {
var rho, theta;
Point(this.rho, this.theta);
get x => rho * cos(theta);
get y => rho * sin(theta);
scale(factor) => new Point(rho * factor, theta);
operator +(p) => new Point(x + p.x, y + p.y);
static distance(pl, p2) {
var dx = pl.x - p2.x;
var dy = pl.y - p2.y;
return sqrt(dx * dx + dy * dy);
}
}
现在,客户端都不需要修改代码了。getter 的调用语法与变量的访问没有区别。
答案是它不知道。 Dart中所有实例变量的访问,实际上都是调用getter 。 每个实例变量始终有一个与之关联的getter, 由Dart 编译器提供。
如果客户端给字段赋值,比如:
myPoint.y = myPoint.y * 2;
新版Point 类没有对应的可以被赋值的字段。客户端不清楚怎么修改并使其代码保待运行。 为了解决这个问题,我们使用setter 方法(简称为setter) 。
setter 方法名前面要添加前缀set, 并只接收一个参数。 setter 的调用语法与传统的变量赋值是一样的。 如果一个实例变量是可变的,则一个setter 将自动为它定义,所有实例变量的赋值实际上都是对setter 的调用。
class Point { var rho, theta; Point(this.rho, this.theta);
get x => rho cos(theta); set x(newX) { rho = sqrt(newX newX + y * y); theta = acos(newX / rho); }
set y(newY) { rho = sqrt(x x + newY newY); theta = asin(newY / rho); }
get y => rho * sin(theta);
scale(factor) => new Point(rho * factor, theta);
operator +(p) => new Point(x + p.x, y + p.y);
static distance(pl, p2) { var dx = pl.x() - p2.x(); var dy = pl.y() - p2.y(); return sqrt(dx dx + dy dy); } }
>问题还存在第3 个方面。客户端通过调用Point 类的构造函数来创建一个点,如下所示:
```dart
new Point(3,4);
新Point 类的构造函数接收两个参数,它们代表向量的长度和角度,而不是直角坐标。 正如前面提到的, Point 类内部也依赖这个API, 所以它们是无法正常工作的。 像往常一样,解决方法是保留现有的API, 同时根据需要改变表现方式。 因此,我们还是用极坐标来表示点,但为保持接口不变,我们保留已有的接收直角坐标参数的构造函数。
class Point {
var rho, theta;
Point(a, b) {
rho = sqrt(a * a + b * b);
theta = atan(a / b);
}
//...剩余代码不变...
}
现在已经达到改变点的表示方法同时不对客户造成任何影响的目标,并且没有做任何前期规划。我们不需要提前决定是通过accessor 方法还是特定的属性声明来暴露点的坐标, Dart 帮我们做了,而且没有带来任何语法上的不便。 在任意主流编程语言中,我们都能修复这种构造函数的问题。然而,如果没有accessor, 则完全平滑过渡是不可能的。
当一个类声明一个实例变量时,它会确保每个实例都有自己的唯一变量复制。 对象的实例变量需要占用内存,这块内存是在对象创建时分配的。重要的是,此内存在被访问之前,应该被设置为某些合理的值。在低级语言如C 语言中则并不如此,新分配的存储空间的内容可能是不明确的,通常就是内存在重新分配之前的值。这将会导致可靠性、安全性方面的问题。
Dart 会将每个新分配的变量(不只是实例变量,还包括局部变量、类变量和顶层变量)初始化为null 。 在Dart 中,与其他对象一样, null 也是一个对象。 不能把null 与其他对象混淆,如0 或false 。null 对象只是在Dart 核心库中定义的Null 类的唯一实例。 这种情况与其他语言如C 、C++ 、C# 、Java 或JavaScript 是不一样的,但这是一切皆对象的理念所带来的必然结果。
声明实例或静态变量会自动引入一个getter。如果变量是可变的,则一个setter 也会被自动定义。 Dart 中的字段都不是直接访问的,所有对字段的引用都是对accessor方法的调用。 只有对象的accessor 才能直接访问它的状态,所有访问对象状态的代码都必须通过这些accessor 方法,这意味着每个类的底层表示都可以随时更改,且不需要客户端修改代码,甚至重新编译也不需要!这种属性被称为“表征独立“。 在上一节见识了”表征独立”的好处,展示了改变点的表示方式且无须修改任何使用了点的代码。
除了实例变量,类也可以定义类变量。 一个类只有一份类变量的副本,无论它有多少个实例。即使类没有实例,类变量也存在。 类变量的声明是在变量名前放置单词static 。
可以添加一个类变量来跟踪有多少个实例被创建。 在这里,每当类构造函数Box()运行时,它就会增加己创建箱子的数量。
class Box { static var numberOfinstances = 0; Box() { numberOfinstances = numberOfinstances + 1; } }
像实例变量,类变量从不直接引用。所有对它们的访问都是通过accessor 。 在它的声明类中,类变量可以通过名称直接引用。 在类的外部,只能通过在变量名前加上类名来访问:
fn() { Box.numberOfinstances == 0 ? print('No boxes yet') : print('We haveboxes!'); }
类变量通常也被称为静态变量,但“静态变量”这个术语也包括了类变量与顶层变量。 为了避免混淆,我们将坚持使用“类变量”这个术语。 我们也经常会用”字段”这个术语来统称实例和类变量。
在getter 第一次被调用时类变量才执行初始化,即第一次尝试读取它时。 与其他变量一样,如果一个类变量没有被初始化,则它会默认初始化为null 。
假设一个类变量在被赋值之前就被读取,就像下面的例子(注:例子中使用的是顶层变量而非类变量,但它们的行为是一 致的) : 此处, schrodingers 的初始化永远不会执行,并且对print()的调用也永远不会执行。 程序将抛出异常
class Cat {}
class DeadCat extends Cat {}
class LiveCat extends Cat { LiveCat() { print("I'm alive!"); } }
// 延迟初始化 var schrodingers = new LiveCat();
main() { schrodingers = new DeadCat(); }
>虽然以上情况看起来可能是非常明显的,但在更复杂的情况下就未必了。
>例如,在调试的过程中,开发者可能会检查变量的值,那样就会触发变量的初始化。
>开发者应该始终密切关注延迟初始化带来的影响。
***
## final 变量
>Dart 的变量可以用单词final 作为前缀,表明它们在初始化后不能再修改。
>final 字段有 getter 但是没有setter.
> final 类变量必须在声明时就进行初始化。
>final 实例变量必须在任意实例方法运行前进行初始化。
>实现这一点有几种方法。第一种是在声明变量时就进行初始化,例如:
```dart
class A {
final origin = new Point(0, 0);
}
这种方式不一定总是很方便的。 在不同的构造函数中,设置这个变量的方式可能不一样。 例如,这个变量可能取决于构造函数的参数。 如果想把fmal 实例变量设置为构造函数参数的值,则可以使用普通的构造函数简写。 作为例子,考虑以下Point 类,它表示不可变的点:
class Point { final x, y; Point(this.x, this.y); // Point 类的剩余代码... }
不过,某些情况下这还不够。变量的值可能会取决于构造函数的参数,但又不完全相同, 也就是说,它们的值是基于构造函数参数计算得来的
试图给一个final 实例变量赋值通常会导致一个名为NoSuchMethodError 的错误,因为赋值操作只是调用setter 的语法糖,而final 实例变量所对应的setter 方法是未定义的。 单独声明一个对应的 setter 是可行的,它也会被调用。然而这对实例变量的值没有任何影响,在final 变量初始化之后,它的值就无法改变了。
大部分实例变量在声明时就被赋值并不再改变。你可能为此感到惊讶,但这已经被系统研究验证了。 因此,大部分实例变量最好都声明为final 。 有一种强烈的观点是final 应该被设置为默认,但是那样会违背已有的习惯,所以Dart在这里选择了传统的做法。
所有对象都支待相等操作符=。这个操作符是在Object 类中定义的,因此所有的类都继承了它,所以所有对象实例的行为都包含了它。考虑以下代码:
main() { var aPoint = new Point(3, 4); var anotherPoint = new Point(3, 4); aPoint == anotherPoint; //值为false }
Object 类的
==
方法用于检测参数与接收者是否相同。 每个对象都有唯一标识,一个对象只与它自己相同。 从上面的例子可以看出,两个对象可以是同一个类的实例,并且有相同的状态,但它们仍不相同。我们说两个对象不相等是因为相等被定义为相同,但是这回避了为什么要如此定义的问题。 ,决定实例如何才是有意义的相等,是定义类的开发者的责任。做到这一点的方法是重写
==
。 在Point 类中,我们可以这样定义相等;operator ==(p) => x == p.x && y == p.y;
某些情况下,开发者可能希望检查两个表达式是否代表相同的对象,但这比较少见。 开发者通常只检测对象是否相等。
dart:core
库中定义了一个identical()
方法,开发者可以使用它来检查两个对象是否相同。identical(origin, origin) 的值为true, identical(aPoint, aPoint) 与identical(anotherPoint, anotherPoint)的值也是一样的。 另一方面,identical(aPoint, another Point)的值为false 。
来定义Object 类的相等方法:
bool operator ==(other) => identical(this, other);
所有Dart 对象都支持一个名为hashCode 的getter 方法。 对象的相等和hashCode 是相互关联的。如果两个对象相等,那么它们的哈希码也应该相等,代码的实现者必须小心维护这个属性。 在实践中,这意味着如果你选择重写上述两个方法中的一个,那么你也应该重写另一个。
实现自定义的相等必须要谨慎。 我们期望我们的相等具备:
自反性
(a=a)
、 可传递性((a = b) && (b = c)
意味着(a=c)
、 互换性(a =b)
意味着(b=a)
。其中,自反性是你可以在自己实现
==
方法时确保的,而其他属性在不断扩展的系统内是比较难以维护的。开发者总是可以引入类似的代码:
class BadApple { operator ==(x) => true; }
以上代码会逐步破坏整个系统的相等属性。
每个类都声明了一组实例成员,包括实例变量和各种实例方法。 每个类(Object类除外)继承了父类的实例成员。
由于除了Object 类外的所有类都只有一个父类,而Object类没有父类,所以Dart 类层次结构形成了一个以Object 类为根的树。这种结构叫作单继承, 如下图所示。这只是整个Dart 类层次的一个小片段。
如果子类声明一个与父类的某个方法同名的实例方法,那么可以说成子类重写了父类的方法。
你不能用一个普通方法重写getter, 反之亦然。这些情况会导致编译错误。
class S { var v; final f = 0; get g => 42; set s(x) => v = 2; m(a, b) => 91; }
class C extends S { V() => 1; // 非法:方法v( )重写隐含的getter 方法v f() => 2; // 非法:方法f ()重写隐含的ge 七ter 方法f g() => 100; // 非法:方法g( )重写隐含的getter 方法g }
#### 试图用方法或 getter 重写 setter ,或者用 setter 重写方法或 getter 在技术上都是不可行的。
>如果你尝试,则Dart 会警告你:
```dart
class D extends S {
s(y) => 200; //警告: D 有方法s 和setters=
}
当一个重写方法比被重写的方法需要更多的参数时, Dart 编译器将产生一个警告,但是代码仍然可以编译。
class E extends S { m(x, y, z) => 101; // 警告:重写方法参数个数不一致 }
在这种情况下,为什么Dart 只发出警告而不马上拒绝这样的代码呢? 因为Dart 极力避免给开发者强加工作流程。 各种不一致的情况都能在开发中出现,并且它们最终都应该被修正。然而,在不能取得任何进展前,强迫开发者马上处理这些情况,往往适得其反。 因此, Dart 确保开发者能够通过警告知道这些问题,但只在绝对必要的情况下才会中止编译。
就运行时而言,抽象方法根本不存在。毕竟,它们没有实现,也无法运行。 调用抽象方法就与调用一个不存在的方法一样。 只包含抽象方法的类在定义接口时很有用
通常来说,简单地声明一个方法而不提供它的实现是有用的,这种方法被称为抽象方法。 任何种类的实例方法都可以是抽象的,不管它是getter 、setter、操作符或普通的方法。 声明一个抽象方法将告诉代码的阅读者(人或者电脑),这个方法只在代码运行时才可用。 这能帮助开发者理解代码,同时有利千对错误的处理。
有一个抽象方法的类本身就是一个抽象类,抽象类的声明是通过在类名前加上前缀 abstract 。
一个抽象类,它不包含任何实现信息,纯粹作为一个接口,如下所示。
abstract class Pair { get first; get second; }
类Pair 有两个抽象的 getter 方法 first 和 second 。 Pair 被显式声明为抽象类。 如果删掉 abstract 修饰符,那么Dart 解析器会发出这个类具有抽象方法的警告。 抽象类有抽象方法是完全正确的,但如果这个类是可实例化的类,那么显然是个问题。 abstract 修饰符能让我们宣告自己的意图,而Dart 解析器也会根据情况做出相应的改变。
抽象类不是被用来实例化的,毕竟,它缺失部分实现。 对它进行实例化会导致运行时错误,具体来说,会产生一个名为AbstractClassinstantiationError 的错误。Dart 解析器也会 对此发出警告。
new Pair(); //静态警告:试图实例化一个抽象类 //抛出AbstractClassinstantiationError 错误
每个类都隐含地定义了一个接口,此接口描述了类的实例拥有哪些方法。 很多编程语言都有正式的接口声明,但在Dart 中没有。这是不必要的,因为我们始终可以定义一个抽象类来描述所需的接口。
abstract class CartesianPoint { get x; get y; }
abstract class PolarPoint { get rho; get theta; }
### implements
>尽管没有了接口声明,类也能使它的实例实现特定的接口:
```dart
class Point implements CartesianPoint, PolarPoint {
// 这里是Point 类的实现代码
}
以上Point 类不是CartesianPoint 的子类, 它没有继承CartesianPoint (或PolarPoint) 的任何成员。 implements 的目的是在接口间建立预期的关联,而不是共享实现。
main() {
5 is int; // true
'x' is String; // true
[] is Point; // false
aPoint.toString() is String; // true
new Point(0, 0) is String; // false
aPoint is CartesianPoint; // true
}
请注意, is 不检查对象是否为某个类或其子类的实例。 相反, is 检查对象的类是否明确地实现了某个接口(直接或间接)。 换句话说,我们并不关心对象是如何实现的,我们只在意它支持哪些接口。 这是与其他有类似构造的语言的关键区别。 如果一个类希望模拟另一个类的接口,则它并不局限千已有的实现。这样的模拟对客户端而言应该是不可区分 的(除非使用反射)
类的隐含接口会继承父类的隐含接口,同时会继承父类实现的接口。 同类一样,接口可以重写父接口的实例方法; 另外,某些重写可能是非法的, 例如重写方法与被重写方法的参数不一致,或者试图用普通方法重写getter 或setter, 反之亦然。
假设一个同名的方法在多个父接口中出现,而且它们的参数不一致,则在这种情况下,互相冲突的方法没有一个会被继承 如果一个父类定义了一个getter,而另一个父类也定义了同名的普通方法,那么结果也是一样的。 这些情况也会引起各种警告。
Dart 中的计算都是围绕对象展开的。因为Dart 是纯面向对象的语言,所以即使是最微不足道的Dart 程序也会涉及对象的创建。 举个例子,一个字符串对象的创建。
某些对象,比如字符串'Hello World' 、布尔量true 或数字91 都是字面量。
大多数对象都是由实例创建表达式创建的 比如new Point(O, 1)。 这种表达式调用了一个构造函数,这里是Point()。 每个类至少有一个构造函数,构造函数的名称总是从我们想创建实例的类名开始的。
构造函数可以由开发者明确地声明,或者也可以隐含地产生。 在没有明确的构造函数被声明时,隐含的构造函数将被创建,它们没有参数和函数体。例如:
class Box {
var contents;
}
等同于:
class Box { var contents; Box(); }
以上类又等同千:
class Box { var contents; Box() {} }
Point(a, b) {
x = a;
y = b;
}
评估一个实例创建表达式的第1·步是评估构造函数的参数。 在new Point(0,1)中,参数是字面量的整数0 和1 。 同其他函数调用一样,形式参数被设置为实际参数的值,所以a设置为0 且b 设置为1 。 现在我们能够分配一个新的Point 类的实例,它存储了两个字段x和y。 起初,这两个字段将被系统设置为null, 这确保了用户的代码永远不会遇到未初始化的内存。 现在可以执行构造函数的函数体了,其中涉及两个变量即x 和y 的赋值操作。 这些赋值操作其实是调用setter 方法。 字段x 和y 被赋值为0 和1 ,其实是隐含定义的setter 执行的。 此时,构造函数返回一个新创建对象的引用,这也是new 表达式的最终结果。
考虑一个类,它代表三维空间的点。这个类有x 、y 和z 三个坐标,并且很自然地被声明为Point 类的子类。
class Point { final x, y; Point(this.x, this.y); }
class Point3D extends Point { var z; Point3D(a, b, c) : super(a, b) { z = c; } }
>一个Point3D 的实例可以通过new Point3D(1, 2, 3)来创建。
>Point3D 类有三个字段:
>>Point 类继承的字段x 和y, 在Point3D 中声明的字段z
>所有这三个字段将再次被设置为null 。
>但是,在执行Point3D 的构造函数的函数体之前,我们将不得不执行Point 的构造函数的函数体,否则字段x 和y 将不会被正确初始化。
>一般来说, Dart 编译器不能确定应该传递怎样的参数给父构造函数
>最简单的情况下(父构造函数没有参数),我们需要调用一个明确的父构造函数来指引我们,在上面的例子中就是super(a, b) 。
>在父构造函数调用中, super 代表了父类的名称,也就是Point3D(x1, y1, z1)在执行自身的构造函数的函数体之前调用了Point(x1, y1)的构造函数的函数体。
#### 初始化列表
>其目的是在普通代码运行前对实例变量进行初始化。
>假定点是不可变的,但会使用极坐标的表示方式。
>它执行了两个初始化操作:一个是rho, 一个是theta。
```dart
import 'dart:math';
class Point {
final rho, theta;
Point(a, b)
: rho = sqrt(a * a + b * b), //初始化列表
theta = atan(a / b); //初始化列表
get x => rho * cos(theta);
get y => rho * sin(theta);
scale(factor) => new Point(rho * factor, theta);
operator +(p) => new Point(x + p.x, y + p.y);
static distance(p1, p2) {
var dx = p1.x - p2.x;
var dy = p1.y - p2.y;
return sqrt(dx * dx + dy * dy);
}
}
这里的初始化列表是从
Point(a, b)
后的冒号开始直到该行结尾的分号:rho= sqrt(a * a + b * b), theta= atan(a/b)
。初始化操作用逗号分隔,并从左到右执行。 除了初始化实例变量外,初始化列表也可以包含一个父构造函数的调用。在Point3D()中看过了这一点,即它使用了这个初始化列表:
super(a,b)
。 如果初始化列表中没有调用父构造函数,那么一个隐含的父构造函数super()
将会被添加到初始化列表的尾部。
class Point {
var x = 0, y = 0;
}
class Point {
var x, y;
Point(this.x, this.y);
}
class Point {
var x, y;
Point(a, b)
: x = a,
y = b;
}
该方式不适用千final 实例变量,因为它使用了final 变量所没有的setter 方法。final 实例变量只能初始化一次
class Point { var x, y; Point(a, b) { x = a; y = b; } }
对于一个普通的实例变量,可以选择以上任意一种或多种方式,或者都不使用 当对象被实例化时,各种初始化构建操作将按照上面列出的顺序执行。
假设Point3D 还是按照前面的定义,而Point 类有如下构造函数:
Point(a, b) : x = a, y = b;
class Point { var x, y; Point(a, b) : x = a, y = b; }
class Point3D extends Point { var z; Point3D(a, b, c) : super(a, b) { z = c; } }
>则这个实例的创建过程如下图所示,执行顺序依照箭头的方向:
![image](https://user-images.githubusercontent.com/30850497/122889509-70adba00-d375-11eb-971f-48ec74ba475f.png)
##### 左侧
>通过new Point3D(7, 8, 9)创建一个Point3D 的实例,从计算实际参数开始,这里的参数是7 、8 和9, 然后构造函数Point3D()被调用。
>下一步是分配一个新的Point3D 实例。所有实例变量被设置为null,
>然后,继续执行Point3D 的初始化列表。这导致父类执行初始化,进而导致Point 的初始化列表开始执行,并将使实例变量x 和y 的值被设置,
>然后执行被隐含添加在Point 类初始化列表尾部的父类初始化操作。这将调用Object 的初始化列表,它什么也没有做(它最后甚至没有调用父类进行初始化)。
>所有这些步骤都显示在上图的左侧。
##### 右侧
>在遍历执行完所有父类链的初始化列表后,下一步就是执行构造函数的函数体。
>在该图中,这对应箭头的掉头。
>一个构造函数的函数体在开始前总是隐含地运行父类构造函数的函数体。
>传递给父构造函数的参数跟初始化列表中调用父构造函数的参数相同,它们不会重新计算。
>因此,在我们的例子中,我们开始运行Point3D(),它又开始运行Point(),进而导致运行什么都没有做的Object()。
>因为Point()没有函数体,所以我们返回到Point3D()并初始化变量Z,
>完成后我们返回新分配的对象。
>以上处理过程对应上图的右侧。
### 重定向构造函数
>Point()现在是一个重定向构造函数。
>重定向构造函数的目的是把执行重定向到另一个构造函数,在这里是Point.polar()。
>在重定向构造函数中,参数列表跟在一个冒号后面,并以this.id(..)的形式指定重定向到哪个构造函数。
```dart
import 'dart:math';
class Point {
var rho, theta;
Point.polar(this.rho, this.theta);
Point(a, b) : this.polar(sqrt(a * a + b * b), atan(a / b));
get x => rho * cos(theta);
set x(newX) {
rho = sqrt(newX * newX + y * y);
theta = acos(newX / rho);
}
set y(newY) {
rho = sqrt(x * x + newY * newY);
theta = asin(newY / rho);
}
get y => rho * sin(theta);
scale(factor) => new Point.polar(rho * factor, theta);
operator +(p) => new Point(x + p.x, y + p.y);
static distance(pl, p2) {
var dx = pl.x - p2.x;
var dy = pl.y - p2.y;
return sqrt(dx * dx + dy * dy);
}
}
假设想要避免分配过多的点。我们想保留点的一份缓存,而不是每次请求都生成一个新的点。 当有人尝试分配一个点时,我们就检查缓存中是否存在相等的点,如果有,就返回那一个点。 一般来说,构造函数使上述设想比较难以实现 在大多数编程语言中,构造函数总是会分配一份新的实例。 如果你想使用缓存,那么你必须提前考虑好,并确保你的点是通过一个方法调用来分配的,而这个方法通常叫作工厂方法。
在Dart 中,任意构造函数都可以被替换为工厂方法,并且对客户是完全透明的。我们通过工厂构造函数来做到这一点。
工厂构造函数由factory 前缀开头。 看起来像普通的构造函数,但可能没有初始化列表或初始化形式参数。 相反,它们必须有一个返回一个对象的函数体。 工厂构造函数可以从缓存中返回对象,或选择分配一个新的实例。 它甚至可以创建一个不同类的实例(或者从缓存或其他数据结构中查找它们)。
只要生成的对象符合当前类的接口,则一切都会按预期执行。
通过这种方式, Dart 解决了传统构造函数的一些典型缺点。
Dart 中的计算都是围绕对象方法的调用。如果调用了一个不存在的方法,则默认的行为是抛出NoSuchMetbodError 错误。但是,并非总是如此。 当调用一个实例中不存在的方法时, Dart 运行时会调用当前对象的noSuchMethod()方法。 因为Object 类noSuchMethod()方法的实现就是抛出NoSuchMethodError 错误,所以我们通常都会看到这个熟悉的行为。 这个方案的优点在于noSuchMethod()能够被重写。 例如,如果你要实现另一个对象的代理,那么你可以定义代理的noSuchMethod()方法,并把所有的调用都转发给代理的目标。
class Proxy { final forwardee; Proxy(this.forwardee); noSuchMethod(inv) { return runMethod(forwardee, inv);} }
noSuchMethod()的参数是Invocation 类的一个实例,它是定义在核心库中的一种特殊类型,用千描述方法的调用。 一个Invocation 反映了原始的调用,描述了我们试图调用方法的名称、传递的参数及其他一些细节。 为了真正把每个调用转发给forwardee, 我们在noSuchMethod()的实现中使用了一个辅助函数runMethod(),它接收一个接收对象和一个invocation, 并使用提供的参数调用接收对象上对应的方法。 在讨论反射的时候,会介绍如何实现runMethod()方法。 健壮的代理实现比上面的代码要复杂一些的。一个细微之处是Proxy 不会转发在Object类中定义的方法,因为这些方法是继承的,并不会导致noSuchMethod()被调用。Object 的接口被设计得很小,可以手动拦截处理。 即使有这些复杂性, Proxy 的完整实现还是很简单的。 编写忽略目标类型的通用代理的能力,是Dart 这种可选类型语言的灵活性的完美体现。这种灵活性是强制类型的语言无法提供的。
有些对象是在编译时就可以计算的常量。 其中许多是显而易见的:
字面量,如3.14159; 字符串,如“Hello World" 等。
常量对象的创建是使用const 而不是new 。
同new 表达式一样, const 表达式也是调用构造函数,但该构造函数必须是常量构造函数,而且它的参数必须是常量。
Dart 要求常量构造函数的参数必须是数字、布尔量或者字符串。 幸好,数字字面量如0 等始终是常量。
一个常量构造函数不能有函数体。它可以有一个初始化列表,前提是只计算常量(假设参数是已知的常量)。
并不是总需要创建常量。仍然可以使用new 调用常量构造函数。如果那样做,则我们传递的参数不再受限制,但结果不再是常量。
常量的值可以提前计算,只需一次,无须重新计算。Dart 程序中的常量是规范化的,一个给定的值只会产生一份常散。
class Point {
final x, y;
const Point(this.x, this.y);
}
const origin = const Point(0, 0);
在适当的条件下,我们可以生成一个代表原点的Point 常量对象。 origin 变量被声明为常量。我们只能把一个常量赋给它。
将Point 的构造函数声明为常量。这会给类及构造函数强加一些非常严格的限制。我们需要一个状态不可变的类。 幸运的是,不可变的点是非常自然的。
不能将类似Point.polar()的构造函数定义为常量,因为它使用了像
sqrt()
这样的结果不是常量的函数。Point 有这样一个构造函数是没有问题的,只是不能用const 调用它而已。因此,任意使用这个构造函数创建的点都不是常量。
类方法是不依赖于个体实例的方法 通过类变量而引入的accessor 都是类方法,我们可以称它们为类getter 和setter
除了自动引入的类accessor, 开发者也能显式地定义这些accessor。
把普通方法定义为类方法也是可以的。 例:在Point 类中有:
static distance(pl, p2) { var dx = pl.x - p2.x; var dy = pl.y - p2.y; return sqrt(dx * dx + dy * dy); }
就像类变量accessor 一样,用户定义的类方法在声明它们的类中是可用的, 在类外只能将它们所在的类作为前缀才能访问,例如
Point.distance(aPoint, anotberPoint)
。在类方法中使用this 将导致编译错误。 因为一个类方法不特定于任意实例,所以this在其内部是未定义的。
如果你尝试调用一个不存在的类方法,那么你会得到一个运行时错误:
Point.distant(aPoint, anotherPoint); // NoSuchMethodError!
因为这是一个静态调用,所以Dart 编译器在编译时能检测到Point 没有distant()方法
因为类方法永远不会被继承,所以声明一个抽象的类方法就没有意义了。 如果尝试,则你将得不到任何进展,因为这在语法上是非法的。
class ExtendedPoint extends Point { var origin = new Point(0,0); get distanceFromOrigin => distance(origin, this); // 对不起, NoSuchMethodError! }
getter 方法distanceFromOrigin 不能使用distance 方法。 我们本可以把distanceFromOrigin定义在Point 中(假设我们同时定义了origin) ,但是小stance 方法在Point 的子类中都不可 见。
为了使这段代码工作,必须这样写:
get distanceFromOrigin => Point.distance(origin, this); // ok
每个对象都是一个类的实例。 类也是对象 类是对象,它们本身也是某个类的实例。类的类通常被称为元类。
Dart 语言指定类的类型为Type,但没有指明它们属于哪个类。
在一个典型的实现方式中,类可能是一个私有类_Type 的实例。 通常来说_Type 自身的类就是_Type, 也就是说,_Type 是自身的一个实例。 这解决了对类型可能进行的无穷无尽的追溯。
对象aC 是类C 的一个实例,类C 又是_Type 的一个实例,_Type 是它自身的一个实例 把Type 用下画线开头,表明它是一个私有类
反射是唯一可靠的发现对象所属类的方式
对象都支待一个名为runtimeType 的getter, 它默认返回对象的所属类。但是,子类可以随意重写runtimeType 。
Type.runtimeType; // NoSuchMethodError
(Type).runtimeType; //正常工作
第1 行调用了一个不存在的类方法,类Type 没有名为runtimeType 的类方法。
第2 行调用了类型字面量Type 的所有对象中都定义了的runtimeType 方法。
假如我们把类方法看作类对应Type 对象的实例方法,则可以解决这个问题,但此时的情况不是这样的。
所有对象共享的接口只由5 个方法组成
操作符方法
==
getter 方法
hashCode
runtime Type
toString()
toString(),这个方法返回一个对象的字符串表示。它的默认版本通常会打印出类似“An Instance of C "的字符串,其中C 是对象的类名。我们通常都会重写这个方法以使它更有意义。
Object 类的轮廓类似于这样:
class Object { bool operator ==(other) => identical(this, other); external int get hashCode; external String toString(); external noSuchMethod(Invocation im); external Type get runtimeType; }
除操作符方法
==
之外的所有方法都由Dart 底层实现,并不直接使用Dart 代码实现。它们被标记为external, 表明它们的实现是在其他地方。
external机制用于声明代码的实现来自于外部。 这些外部代码可以有多种提供方式:
通过作为底层实现基础的外部函数接口(这里就是这样), 或者甚至可能动态地生成实现。
对大多数开发者而言,第1 种情况是最有可能的。
单继承有很大的局限性
每个Dart 类都有一个mixin。 这个mixin 是在类主体中定义的特定功能
可以把 mixin 看作一个函数,它接收一个父类S 并返回一个新的拥有特定主体的 S子类
//非法代码:只是作为演示 mixinCollection (S) { return class Collection extends S { forEach (f); where (f); }}
每次用某个特定的类调用这个函数,都会产生一个新的子类。 因此,我们把mixin 应用看作用一个mixin 与一个父类来派生一个新类。 我们把M 与父类S 的mixin 操作写成S with M S 必须指定一个类 如何指定mixin? 通过指定一个类,每个类都通过它的主体隐含定义了一个mixin, 就是使用它来对S 执行mixin 操作。
class CompoundWidget extends Widget with Collection{ // }
作为mixin 的类不能有显式声明的构造函数,违反这个限制将会导致编译错误。此限制在未来可能会放松。
考虑一个被称为表达式问题的经典设计挑战,同时看一下mixin 是如何帮助我们完成一个优雅的解决方案的
表达式语言:
Expression - Expression + Expression | Expression - ExpressionlNumber
这个语言可能有如下形式的AST (译注:抽象语法树)
class Expression {}
class Addition extends Expression { var operandl, operand2; }
class Subtraction extends Expression { var operandl, operand2; }
class Number extends Expression { int val; }
#### 假设你要为这个语言定义一个求值器,
>可以依照经典的面向对象风格来定义
>即给以上每个子类添加一个eval 方法:
```dart
get eval => operandl.eval + operand2.eval; //在Addi巨on 类中定义
get eval => operandl.eval - operand2.eval; //在Subtraction 类中定义
get eval => val; //在Number 类中定义
以上实现方式是有问题的。 当你想把这些表达式转换为字符串时,你就需要添加另一个方法到原先的层次结构中。 类似功能的函数可能有无数个,你的类很快就会变得难以维护与使用。
还有一个问题是,并不是所有想添加新功能的人都可以访问到原始源代码。
另一种实现方式是把求值器定义为AST 类外部的一个函数。 但这样的话,你不得不对传递过来的表达式进行类型检查,然后执行相应的动作。 这样做烦琐且效率低下, 所以一般会使用访问者模式来替代。 无论如何,这样的代码结构有个双重问题:尽管添加功能很容易,但是添加新类型很难。
下面的表格展示了目前的困难局面。
Class\Function | eval | toString() |
---|---|---|
Addition | operandl.eval + operand2.eval | '$operand! + $operand2' |
Subtraction | operandl.eval - operand2.eval | '$operand! - $operand2' |
Number | val | '$val' |
Multiplication | operandl.eval * operand2 | '$operandl * $operand2' |
类对应表格中的行,功能对应列。 面向对象的风格可以很容易地添加行,添加列却是侵入式的。 函数式风格的解决方案则正好相反:添加列很容易,添加行却是侵入式的。
我们真正需要的是一种把各个条目独立地填入表格的方法。使用mixin 可以很好地解决这个问题
从三个初始数据类型开始,只是不把它们定义为抽象类,因为它们并不是我们最终要实例化的数据类型。 abstractExpressions.dart
library abstract_expressions;
abstract class AbstractExpression {}
abstract class AbstractAddition { var operandl, operand2; AbstractAddition(this.operandl, this.operand2); }
abstract class AbstractSubtraction { var operandl, operand2; AbstractSubtraction(this.operandl, this.operand2); }
abstract class AbstractNumber { var val; AbstractNumber(this.val); }
##### 定义第1 个功能:求值器
>通过一组mixin 类来做到这一点。
>求值器完全独立于类型层次结构,没有导入哪怕一个依赖。
>evaluator.dart
```dart
library evaluator;
abstract class ExpressionWithEval {
get eval;
}
abstract class AdditionWithEval {
get operandl;
get operand2;
get eval => operandl.eval + operand2.eval;
}
abstract class SubtractionWithEval {
get operandl;
get operand2;
get eval => operandl.eval - operand2.eval;
}
abstract class NumberWithEval {
get val;
get eval => val;
}
每个具体的AST 类型都被定义成一个mixin 应用,即用相应的求值器mixin 来扩展对应的抽象数据类型。 expressions.dart
library expressions;
import 'abstractExpressions.dart'; import 'evaluator.dart';
abstract class Expression = AbstractExpression with ExpressionWithEval;
class Addition = AbstractAddition with AdditionWithEval implements Expression;
class Subtraction = AbstractSubtraction with SubtractionWithEval implements Expression;
class Number = AbstractNumber with NumberWithEval implements Expression;
##### 一个简单的表达式树
>可以给 expressions 库添加一个main()函数,用来构建一个简单的表达式树。
>这是可能的,因为各个AST 节点类的父类构造函数都隐含地为它们定义了合成的构造函数。
```dart
main() {
var e = new Addition(new Addition(new Number(4), new Number(2)),
new Subtraction(new Number(10), new Number(7)));
}
expressions 库即实际类型的作用是通过mixin 应用连接各个组件来定义我们的整个系统。
abstractExpressions 库即抽象类型的作用是定义我们的AST 节点的形式。
保持它们的独立,使我们在扩展系统时只需对expressions 库做修改,无须触碰我们的数据类型的表现形式。
一般的模式是,每个具体类都基于扩展一个定义其数据表示的抽象类, 同时用一系列的mixin 来代表该数据类型所具备的功能。
这种方法之所以有效,是因为我们为每个类型和功能的组合单独定义了一个mixin 。 例如,上面的每个eval 方法都是在各自的mixin 类中定义的。
如果我们想增加一种类型,那么我们可以独立地添加。 下面我们将增加乘法的 AST 节点: 这个添加操作同样完全独立千原先的类层次结构和已有的功能。 multiplication.dart
library multiplication;
abstract class AbstractMultiplication { var operand1, operand2; AbstractMultiplication(this.operand1, this.operand2); }
>定义乘法是如何求值的。
>我们可以单独定义一个库:
>multiplicationEvaluator.dart
```dart
library multiplication_evaluator;
abstract class MultiplicationWithEval {
get operand1;
get operand2;
get eval => operand1.eval * operand2.eval;
}
在expressions 库中创建相应的具体类。 它遵循与其他所有类型一样的模式
import 'multiplication.dart'; import 'multiplicationEvaluator.dart';
class Multiplication = AbstractMultiplication with MultiplicationWithEval implements Expression;
##### 将我们在main()方法中构建的树打印出来
>可以通过修改main()来做到这一点,
>也可以通过修改我们的代码以使用新添加的类型。
```dart
main() {
var e = new Addition(new Addition(new Number(4), new Number(2)),
new Subtraction(new Number(10), new Number(7)));
print('$e = ${e.eval}'); // Instance of 'Addition' = 9
}
所打印内容的信息量比我们想象的要少,因为e 的打印使用的是从Object 继承来的默认toStringQ实现。 为了解决这个问题,我们可以把一个专门的toString()实现添加到我们的类层次结构中。
string_converter.dart
library string_converter;
abstract class ExpressionWithStringConversion { toString(); }
abstract class AdditionWithStringConversion { get operand1; get operand2; toString() => '($operand1 + $operand2)'; }
abstract class SubtractionWithStringConversion { get operand1; get operand2; toString() => '($operand1 - $operand2)'; }
abstract class NumberWithStringConversion { get val; toString() => '$val'; }
abstract class MultiplicationWithStringConversion { get operand1; get operand2; toString() => '($operand1 * $operand2)'; }
>再次,我们按照每一个功能、类型的组合定义一种mixin 的方式
>要改进expressions 库来整合新的功能
>expressions.dart
```dart
library expressions;
import 'abstractExpressions.dart';
import 'evaluator.dart';
import 'multiplication.dart';
import 'multiplicationEvaluator.dart';
import 'stringConverter.dart';
abstract class Expression = AbstractExpression
with ExpressionWithEval, ExpressionWithStringConversion;
class Addition = AbstractAddition
with AdditionWithEval, AdditionWithStringConversion
implements Expression;
class Subtraction = AbstractSubtraction
with SubtractionWithEval, SubtractionWithStringConversion
implements Expression;
class Number = AbstractNumber
with NumberWithEval, NumberWithStringConversion
implements Expression;
class Multiplication = AbstractMultiplication
with MultiplicationWithEval, MultiplicationWithStringConversion
implements Expression;
main() {
var e = new Addition(new Addition(new Number(4), new Number(2)),
new Subtraction(new Number(10), new Number(7)));
print('$e = ${e.eval}'); // ((4 + 2) + (10 - 7)) = 9
}
我们的main 函数仍然不变,但是它会打印一个描述更好的树:
((4 + 2) + (10 - 7)) = 9
我们可以根据需要把扩展过程继续下去。 只要你想,你就可以添加多种类型、功能,只要像上面一样,把要使用的类型的最终形式定义成mixin 应用。 每个类型所对应的每个功能,都通过一个独立的mixin 类来定义。 添加新功能确实需要修改这些mixin 应用,但是这看起来更像调整你的make 文件以包括新加的功能或类型(译注: make 是C 语言开发中常用的一种构建工具)。 如果新类型和功能都是独立定义的,那么我们始终可以定义一个mixin 把新功能独立地添加到新类型上,并让它们良好地混合在一起。 从前面那个表格来看,每个mixin 表示一个单独的条目,而每个mixin 应用构成该表的一行。
Dart 的对象模型在很大程度上受到了Smalltalk的影响。 Smalltallc 是第1 个纯面向对象的编程语言,这里的许多想法都跟它直接相关。 虽然Dart的元类层次结构比Smalltalk 更简单,但最终指向自己的元类层次结构的概念是始于Smalltalk 的。 与Smalltallc 不同, Dart 的实例由构造函数而不是方法进行创建,遵循的是C++所建立的传统。 但是,我们解决了经典构造函数的主要缺陷,即不能生产不是由类新分配的对象。
noSuchMethod()的使用是以Smalltalk 的doesNotUnderstand 为蓝本的。
表征独立的概念可以追溯到Self 语言。然而, Dart 并不坚待统一引用的原则,因为它还受到了JavaScript 和C 风格语法的影响。
Mixin 起源于某种Lisp 方言的惯用语法。这里使用的mixin 语言模型是由William Cook和吉拉德·布拉查于1990 年提出,并在Strongtalk 中首次实施的。 Strongtalk 是一个创新的Smalltalk 系统,它对Dart 有非常大的影响。 类似的结构也存在于其他语言中,例如Newspeak 、Scala (它们被称为trait) 和Ruby 等。
”表达式问题”是因Philip Wadler 而得名的。
Dart 是一种纯面向对象的基于类的编程语言,这意味着所有运行时值都是对象,且每个对象都是某个类的实例。
对象有状态和行为。状态只能通过特殊的accessor 方法访问: getter 跟setter。 这确保了Dart 对象上的所有计算都是通过程序接口完成的。
类在运行时才具体化,对象本身也必须如此。因此,每个类都是类型为Type 的元类的一个实例。
每个类都至少有一个构造函数,构造函数用于创建对象。
某些对象是常量,这意味着它们在编译时就可以预先计算。
每个Dart 类都有唯一的父类,除了类层次结构的根,即Object。所有Dart 对象都从Object 继承了共同的行为。
Dart 支持基于mixin 的继承:每个类都引入了一个mixin, 它捕获了类本身对类层次结构所做的独特贡献。mixin 使类的代码以模块化方式重用,而不依赖于它在类层次结构中的位置。
Dart 程序是由被称为库的模块化单元组成的
一个“Hello World" 程序。虽然它可能是最简单的,但它终究是一个库。
main() { print('hello world'); }
与我们在points 库中看到的不一样,这里没有一个显示的库声明。 大多数库都有这样的一个声明
对于快速、简单的任务,能够只写一个函数并运行是非常方便的
在上面的例子中,库由一个顶层函数main()构成。 通常来说,一个库由多个顶层声明组成,这些声明可能定义了函数、变量及类型。
library stack1;
final _contents = [];
get isEmpty => _contents.isEmpty;
get top => isEmpty ? throw 'Cannot get top of empty stack' : _contents.last;
get pop => isEmpty ? throw 'Cannot pop empty stack' : _contents.removeLast();
push(e) {
_contents.add(e);
return e;
}
我们有一个顶层变量_contents, 它被初始化为一个空列表。 与实例变量和类变量一样,顶层变量引入了隐含的accessor。 同样,用户的代码不会直接访问变量。 顶层变量是延迟初始化的,与类变量一样,在它们的getter 第1 次被调用时才执行初始化。 在stackl1中,_contents在某个访问它的方法被调用时才被设置为[]。
顶层变量和类变量一起被称为静态变量。它们的区别在于作用域,即在什么范围内能够通过名称对它们进行访问。 类变量的作用域被限制在声明它们的类中(甚至子类也无法访问它们) 顶层变量(也被称为库变量)的作用域覆盖了声明它们的整个库。 库作用域通常由多个类与函数构成。
与类变量一样,顶层变量也可以声明为final, 在这种情况下,它们没有定义setter 且必须在声明时就初始化。 也可以把静态变量(可以是类或库变量)声明为常量,那样的话,它们只能被赋予一个编译时常量且自身被视为不可变。
顶层函数(常被称为库方法)的作用域规则与顶层变量一样,在整个库中都是可用的,它可以是普通函数、getter 和setter。 在各种情况下,函数的函数体都可以使用
=>
加一个表达式的简写方式(如例子中的isEmpty 、top 、pop) ,或者使用由大括号包裹一系列语句(如例子中的push()) 。除了顶层函数与变量,我们也可以声明顶层类。 在Dart 中类声明都是顶层的,因为Dart 不支待嵌套类。
"Hello World" 程序同时是一个脚本示例,是一个可以直接执行的Dart 库。 脚本从main()函数开始执行。 如果一个库没有main()函数,那么根据定义,它就不是一个脚本,自身也不能够被执行。
脚本还有另一个特性。脚本的第1 行可以是以字符
#
开头的一行纯文本。 在某些环境下,这使得各种解释器能够根据文件头部以#
开头的指令来运行这个脚本。
库是Dart 的基础封装单元。以下画线
_
开头的成员都是库私有的。 我们在上面见过一个_contents 库变量。使_contents 私有有助于维护stack1 所引入堆栈抽象的完整性。 因为只有在stack1 内部才能访问_contents, 所以我们确信没有其他代码会改动堆栈的底层实现。CachingClass 所在库之外的代码都无法访问_cache 字段。
class CachingClass { var _cache; operator [](i) { if (_cache[i] == null) { _cache[i] = complicatedFunction(i); } return _cache[i]; } }
这种方案使你(和编译器及其他工具)不必查看某个变量的声明就可以识别它是否私有。
隐私不应与安全混淆。 Dart 的隐私是为了支待软件工程的需要,而不是安全需求。 唯-安全的边界在isolate 之间,在同一个isolate 中,代码的安全性并没有得到保障。
例如,我们想在一个应用中使用 stack1, 可能会这样写:
import 'stack1.dart';
main() { push('gently'); push('harder'); print(pop); print(top); }
>这使得我们的main()函数能够访问stack1 的push()和pop 。
>这个脚本将先打印出harder,然后打印出gently。
>这段代码能够工作的前提是,将库stack1 保存到一个名为 ```stack1.dart``` 的文件中,并与我们的脚本放在同一个文件夹下。
>但是,如果将stackl 保存在别处,例如```http://staxRUs/stackl.dart``` 呢?
>我们可以把导入语句改为:```import'http://staxRUs/stackl.dart';```
>Dart 的导入语句适用于任意URI (Universal Resource Indicators) 。
>尽管如此,我们仍不推荐使用上面的几种URI, 因为只要导入的库的位置发生变化,就会影响到你的代码。
>这些URI 适用千追求速度且不求完美的实验性任务
>而真正严谨的代码需要更多的规则。
### package:
>通常情况下,我们会这样写:```import 'package:stack1.dart';```
>这种```package:```的导入方式会执行一个常驻的封装了代码位置信息的包管理器。Dart 环境通常都自带包管理器
### dart:
>对于Dart 平台自身的库没有必要使用```package:```方式,这些库都是通过```dart:```来访问的,
>例如:```import 'dart:io';```
>其他的例子包括```dart:html, dart:json``` 等。
>无论你使用哪种方式, URI 最好都指向一个真正的库,否则可能产生编译错误。
>该URI也必须是一个不可变的字符串字面量,并且没有使用字符串插值。
### 命名空间
#### 库内可用的对象
>库内可用的对象包含了库本身所声明的对象及通过导入语句从其他库导入的对象。
>```dart:core``` 中定义的对象是隐含导入的。
#### 命名空间:提供给客户的对象
>库内的可用对象与它提供给客户的对象是不同的。
>首先,库从外部导入的对象对库的用户是不可用的。
>此外,库的私有成员对库的调用者是不可用的。
>因此,通常也把库对外可用的对象称为库导出的命名空间。
#### 导入有冲突的变量
>stack2
>设想我们有另一个实现了栈的库stack2 。
>我们可以编写一些代码来测试这两种实现。
>我们可能会这样开始构造脚本:
```dart
import 'package:stack1.dart';
import 'package:stack2.dart';
main() {}
到现在为止都不错。你可以编译这段代码,没有任何问题。
但是,当我们尝试在
main()
方法中使用导入的代码时,我们会遇到麻烦。import 'package:stack1.dart'; import 'package:stack2.dart';
main() { //测试stack1 push('gently'); //静态警告 push('harder'); //静态警告 print(pop); //静态警告 print(top); //静态警告
//测试stack2 push('gently');
///静态警告 push('harder'); //静态警告 print(pop); //静态警告 print(top); //静态警告 }
>你能说出使用stack1 与使用stack2 的代码的区别吗?当然不能,编译器也不能。
>stack2中的方法与stack1中的方法同名了,而把它们导入到同一个作用域造成了无法挽回的歧义。
>Dart 编译器对每个歧义对象的使用都会给出一个警告。
>导入有冲突的变量本身不会出现任何警告,只有在你尝试使用这些歧义对象时才出现警告,
>这与Dart 避免打扰开发者的理念是一致的。
>这也带来另一个好处,即当某个人在你导入的库中新增一个顶层变量时,你的代码不会被轻易破坏。
>如果我们忽略警告并尝试去运行该代码,那么第1 个pushO调用将会导致运行时错误。
>具体地说,是一个NoSuchMethodError 错误被抛出,因为push()没有被明确定义。
#### 为导入提供不同的前缀来进行区分
>克服以上歧义的一种好方法是,为这两个导入提供不同的前缀来进行区分。
```dart
//import 'package:stack1.dart' as stack1;
//import 'package:stack2.dart' as stack2;
import 'stack1.dart' as stack1;
import 'stack2.dart' as stack2;
main() {
//测试stack1
stack1.push('gently'); //静态警告
stack1.push('harder'); //静态警告
print(stack1.pop); //静态警告
print(stack1.top); //静态警告
//测试stack2
stack2.push('gently');
///静态警告
stack2.push('harder'); //静态警告
print(stack2.pop); //静态警告
print(stack2.top); //静态警告
}
Dart 库内声明的对象优先级高于任何导入的对象
导入前缀不得与本库内其他顶层变量声明发生冲突。 编译器会将这些冲突标记为错误。 因为本地变量的优先级高千所有导入的对象,所以导入前缀会稷盖任意不是通过前缀导入所导入的同名变量。
对多个导入使用同样的前缀是允许的。 如果导入语句引入的前缀名称发生冲突,那么在前面看到的变量名冲突规则同样适用千此:只有真正使用了名称冲突的对象时才会导致在编译时产生警告,在执行时也因为报出NoSuchMethod 错误而导致运行失败。
仅仅在指向不同的对象声明时, Dart 才会认为变量冲突。 如果从同一个库中导入两次push(),那么在编译与运行时都不会有问题。
命名空间是名称到声明的映射。 例如,考虑stackl 所声明的命名空间,它包含了_contents 、isEmpty 、top 、pop 和push。 当我们导入stack1 时,因为_contents 是私有的,所以我们看不到它,它不在stack1所导出的命名空间中。 在通常情况下,导入语句将库导出的命名空间提供给导入者访问。 导入前缀和命名空间组合器使我们能够操纵被导入的命名空间。
hide 组合器接收一个命名空间和一个标识符列表,并将标识符列表中的对象从命名空间中丢弃,然后产生一个新的命名空间。
如果我们在一个库中使用以下导入语句:
library lib1; import 'stack1.dart' hide isEmpty, top;
那么只有pop 和push 在lib1中可用,因为这里导入的命名空间不再是stack1 完整导出的命名空间。 hide 操作符被应用到导出的命名空间中,将isEmpty 和top 移除了。 实际提供给导入者作用域的是hide 操作符应用之后的结果。
只有在标识符列表中出现的对象会被保留在命名空间中。 可以使用show 来获得与前面例子同样的效果:
library lib1; import 'stack1.dart' show pop, push;
如果你要导入一个大型的库,而你只想使用其中的少数成员,那么你会发现show 更方便。
相反,如果你试图解决库之间的一两个冲突,那么你可能选择使用hide, 但更好的方式是通过as 引入前缀来避免冲突。
尽管如此,我们将看到hide 的更多用途。
library lib1;
impert 'stack1.dart' as stack1 show pop, push;
通过这种方式,你只要看一眼库的顶部就知道你所依赖的成员。 大多数开发者可能觉得这种约束有一些乏味,但好的开发工具应该能自动为你维护这些导入。
有人可能会觉得通过前缀访问会比较麻烦。 一种替代方案是坚持使用show, 而不使用前缀。 使用show 将防止导入的新成员有意外冲突。 然而采用这种方式时,可能还是需要解决不同导入的成员之间的冲突,并提防导入成员和继承成员之间的冲突。
有时,一个库可能太大,不能方便地保存在一个文件中。 Dart 允许你把库拆分成较小的被称为part 的组件
这个 IDE 包含了类浏览器、对象观察器、调试器及对单元测试、包管理和版本控制的集成支持。 这是一个大程序,我们绝对不想用一个庞大的文件来保存这一切。 同时,因为紧密集成,我们可能希望IDE 是一个库,其自身的私有状态在各子系统间共享。 我们可能有如下结构:
library ide;
import 'dart:io' as io; import 'dart:rnirrors' as mirrors; import 'dart:async' as async; //还有更多:如UI 等. part 'browsing.dart'; part 'inspecting.dart'; part 'debugging.dart'; part 'unitTestintegra巨on.dart'; part 'packages.dart'; part 'vcs.dart';
>每个子系统都存放在各自的文件中,而库通过使用part 指令来引用它们。
>每个 part 指令都给定了一个指向对应part 所在位置的URI。这些URI 与导入语句遵循同样的规则。
>所有part 都共享同一个作用域,即引用它们的库的内部命名空间,而且包含所有导入。
### part 是结构化的,每个part 都必须以一个part 头来指定它属于哪一个库。
>part 指令看起来可能类似C 语言```#include``` 指令,但情况并非如此。
>例如,'brwosing.dart'的开头可能是这样的:
```dart
part of ide;
//顶层声明
class ClassBrowser{
}
part 的头部使用库名称来指明它所在的库。 不是所有的库都有名称, 但如果使用part来构建库,那么库本身必须要命名。 各个part 应该有很好的结构,并且按照逻辑分组,而不是纯粹地堆积代码。
如果库通过URI 引用某个part, 而URI 的内容并不是一个part, 那么这就是一个编译错误, 而如果part 本身不指向同一个库,那么会产生一个警告。与导入一样(导出也是一样的)
集成开发环境不一定具有庞大的百万行级别的代码,但它可能会达到那样的规模。 我们的项目可能不断发展并超出我们最疯狂的期望, 而我们真的需要把它拆分成多个库。 同时,我们可能也想给第三方提供IDE 的API 。 当然,第三方可以根据需要导入IDE 的不同组件库。 但是,这些API 可能是庞大而复杂的,而我们可能希望提供一个更好管理的API 子集。 在我们避免让我们的用户导入大量的子库时,这种方式也很有用。 此外,我们也不想让我们项目的内部库暴露在外。
为一系列的库构建一个可管理的API
library ideAPI;
export 'browsing.dart' show AbstractBrowser, LibraryBrowser, ClassBrowser; export 'inspecting.dart' show Objectlnspector; export 'debugging.dart' show ThreadBrowser, ActivationBrowser; export 'unitTestintegration.dart' show TestBrowser, TestSuite; export 'packages.dart' show PackageBrowser; export 'vcs.dart' show RepositoryBrowser;
### 导出指令 export
>导出指令允许一个库使用来自其他命名空间的对象来扩充自己的导出命名空间。
>在ideAPI 中,库的全部用途就是以一种便于使用的方式来聚集和包装来自不同库的特性。
>它通过使用show 和hide 对几个库的导出命名空间进行了合理过滤,从而构建出一整套API 。
>虽然我们的例子只使用了show,但在我们导出一个大型的API 而又不想与其他导出的库发生冲突时, hide 就非常有用了。
>使用hide 有一个缺点:如果导出的库添加了新成员,则可能会发生冲突。
>hide 的优势是,在我们的客户需要访问我们导出的库的新成员时,我们不需要显式地更新我们的导出语句。
### 导出与导入完全独立
>ideAPI 没有任何导入。
>你可以导出一个库,即使你从没使用过这个库。
>正是因为如此,才使得ideAPI 这样的库能够聚合(拆分)多个API 。
### 导出的规则与导入、part 的规则相同
>导出使用的URI 必须指向一个库,而且必须是不可变的没有使用插值的字符串字面量。
>如果违反了这些要求,那么编译器会报错。
>如果一个库使用多个导出语句来导出同一个对象,那么也是一个编译错误。这种情况在本质上是含糊不清的,我们实际导出的应该是哪个实体呢?
***
## 钻石导入
>从多个聚合性API 导入时,可能会从不同的路径导入相同的对象。这种情况被称为钻石导入
***
## 延迟加载
>推迟库的加载的原因:
>>为了使应用快速启动且保待初始下载量尽可能小
>>在拥有诸多功能的大型应用中,因为某些特性不被所有用户使用,所以实现相应功能的库也不总是需要的。
>>不加载不会使用到的库有助千减少内存的使用。
### 延迟加载 deferred
```dart
import 'rarelyUsed.dart' deferred as rarelyUsed;
在开发时编译器仍然会把导入的对象引入到当前的作用域中, 但在运行时尝试访问这对象则会导致动态错误,除非它们被显示地加载。
延迟加载的导入必须提供前缀,且前缀不能被库中的其他导入使用。 违反这些规则将会导致编译错误。
当真正需要使用延迟加载的库时(例如,当用户选择了需要相应功能的菜单项时), 我们可以调用rarelyUsed 的loadLibrary()方法。
rarelyUsed.loadLibrary().then(onLoad);
onLoad 的定义如下:
onLoad(loadSucceeded) => loadSucceeded ? doStuff() : makeExcuses();
它启动了库的加载,但会立即返回而不等待库加载完成。 loadL如ary()的结果是一个future, 它是某个值的占位符且该值在一段时间后才可用。 future 支待一个then()方法,该方法的参数是一个用于接收值的回调函数。 当future 代表的值最终可用时,该回调函数将被调用,并传入真正的值。
在加载处理完成后(成功或失败), onLoad 函数将被调用。 它的参数loadSucceeded, 即上述布尔量,会告诉我们库是否真正被载入。 然后我们会选择合适的行动方针。
例如, doStufti()可能会这样写:
doStuff() { rarelyUsed.respondToMenuRequest();}
在确定库加载完成之前,我们要避免访问rarelyUsed。 如果在库加载完成之前就访问它,则我们会得到一个运行时错误。 可以选择只在doStufti()中引用rarelyUsed, 从而降低过早访问的风险。 在rarelyUsed 加载之前,我们仍然要面对doStuff()可能在其他地方被调用的风险。
Dart 的导入是遵循传统的,除了依赖千URI 而不像大多数传统语言一样使用内置的标识符。 传统导入的优缺点都被带入到Dart 中。 例如,不可能将同一库的多个副本绑定到同一地址空间中的不同依赖项。 Dart 对isolate 的支持可能会缓解这一问题。 另一方面,这种导入形式是切合实际的,并被大部分开发者认可。
钻石导入与钻石继承间题是有关联的,但它们还是不太相同。
Dart 对隐私的处理方式是原创的。 它偏离了历史悠久的基于类的隐私模型, 例如在C++、Java 及C#中所见。 它也不同千Smalltalk 的基于对象的隐私模型,以及在CLU 、Ada 、Modula 和Oberon 等语言中的基于抽象数据类型(ADT) 的隐私模型。 这种方式是务实的。它很好地处理了多个类之间的交互,而不依赖千类型系统。 在缺少强制静态类型系统的情况下,基于类的封装需要昂贵的动态检测,因此动态类型的面向对象语言都倾向千使用基千对象的封装,或是把所有都设为公开。 前一种选择被认为是过于严格的一项规则,而后者则不适用于为严谨软件工程而设计的编程语言。 命名空间组合器show 和hide 的由来有着悠久的历史。具体例子包括模块组合器、trait的操作、Racket中对单元的操作及其他等。
Dart 程序由库组成。库聚合类(类型)、函数和变量。 Dart 的库是针对隐私而不是安全性的封装单元。 Dart 程序的执行总是从脚本的main()函数开始的。 一个库能被拆分成多个part 。 Dart 库通过导入来接入它们自身的依赖,并能通过执行命名空间组合器来选择性地导入其他库的对象,也能够通过添加前缀来区分各个导入。 库通过命名空间组合器也能将其他库或自身的部分内容重新导出。 库可以延迟到运行时才加载,以改善启动时间及(或者)减少不必要的使用。
函数是Dart 的主力。 所有计算都由函数来执行。 我们将所有语法形式的函数与方法笼统地称为函数,不管它们的表现是否像严格意义上的函数。 函数是Dart 中的一等值,它们能被保存在变量中,能作为参数传递及作为函数的返回值。 与所有Dart 运行时的值一样,函数同样是对象。
多种函数:
顶层函数 实例与类方法(它们可能是getter、setter、运算符或普通方法) 构造函数。 本地函数和函数字面量。
它们都有着共同的结构
函数总是有一个形式参数列表 虽然这个参数列表可能为空,并且getter 方法是没有参数列表的。
参数要么是位置型的,要么是命名型的。
位置参数可以是必填的或可选的。
下面是一些拥有必填参数的函数:
zero() => 0; //没有参数的函数;从技术角度上讲,它有0 个参数
get zero => 0; //上面函数的另一版本
id(x) => x; // identity 函数
identity(x) { return x; } //一个更烦琐的过entity 函数
add(a, b) => a + b; //有两个必填参数的函数
#### 可选参数
>可选参数必须排列在一起放置在参数列表尾部并用方括号包裹。
>任意必填参数都必须出现在可选参数前面。
>可选参数可以指定默认值但必须是编译时常量。
```dart
increment(x, [step = 1]) => x + step; // step 是可选的,默认值是1
以用一个或两个参数来调用increment()
main() { increment(1); //值为2 increment(2, 4); //值为6 increment(); //运行时错误 increment(2, 3, 4); //运行时错误 }
命名参数要在位置参数之后声明并用大括号包裹。
addressLetter()可以使用任意命名参数的组合来调用,包括全部或没有参数的情况。
class Address { var street, number, city, zip, country; }
addressLetter({name: '', street: '', number, city, zip, country}) { var addr = new Address(); addr.street = street; addr.number = number; addr.city = city; addr.zip = zip; addr.country = country; return addr; }
main() { addressLetter(street: "Downing", number: 10); addressLetter(street: "Wall", city: "New York", country: "USA"); addressLetter(city: "Paris", country: "France"); addressLetter(name: "Alice", country: "Wonderland"); addressLetter(); addressLetter(name: "room", number: 101, country: "Oceania"); }
#### 必填参数与命名参数混合的情况:
```dart
var map = new Map();
fail() => throw ('Key not found');
lookup(key, {ifMissing: fail}) {
var result = map[key];
if (result == null) {
return ifMissing();
}
return result;
}
通常,作为错误处理程序的回调函数会通过命名参数来指定,其他形式参数则通过位置参数给出。
lookup("anything"); //抛出错误 lookup("any七hing", ifMissing: () => map["anything"] = 42); //返回值为42
如果想给出更好的错误信息,则我们可能希望这样写: 如果可选参数没有指定默认值,则它的值默认是null,
var map = new Map(); lookup(key, {ifMissing}) { var result = map[key]; if (result != null) { return result; } return ifMissing == null ? throw "$key notfound" : ifMissing(); }
换而言之,对参数的分类如必填或可选,与参数的位置或命名的分类是没有关联的。 不能混合使用可选位置参数与命名参数,你只能使用其中一种。
与其他变量一样,它们能够被修改,但这不是一种好风格,应该避免。 应该尽量减少使用可变的变量,可变将导致代码难千理解与推理。
函数体包含了函数在执行时需要计算的代码。 函数体跟在函数签名之后,而且有两种形式
(1) 大括号括起来的语句列表(可能为空)。 (2)
=>
符号后跟着一个表达式。在第1 种形式中,函数体从第1 条语句开始执行,直到以下任意一种情况发生:
函数最后一条语句被成功执行, 一个return 语句被执行, 或是抛出了一个没有被捕获的异常。
Dart 中的每个函数要么返回一个值,要么抛出一个异常。 如果我们完成了最后一条语句,而且它不是return, 则我们将返回null 。
构造函数是用来创建类的实例的特殊函数。 构造函数包括了工厂构造函数与生产构造函数, 其中工厂构造函数是具备特殊功能的普通函数。 生产构造函数与工厂构造函数的区别在于,它始终返回一个新的实例或者抛出一个异常。所以就算是没有显式地使用return 语句,生产构造函数也不会返回null 。 实际上,生产构造函数不能返回任何表达式,它只可能包含一个没有关联表达式的return 语句。
大多数函数都是通过函数声明来进行介绍的,而构造函数、getter 和setter 例外。 函数声明有一个函数名称,后面跟着参数列表和函数体。
抽象方法有函数签名但是没有函数体。抽象方法从技术上说并不是函数声明。 把它们作为声明只是为了辅助静态检查器。
函数声明可以出现在顶层(例如, main()) 或是作为方法存在,
函数也可以是局部函数。局部函数就是定义在其他函数内部的函数。
为计算斐波那契数列定义一个内部的辅助函数:
fib(n) { lastTwo(n) { if (n < 1) { return [0, 1]; } else { var p = lastTwo(n - 1); return [p[1], p[0] + p[1]]; } }
return lastTwo(n)[1]; }
>这并不是计算第n 个斐波那契数的最佳方式,但是它避免了幼稚的递归计算版本所带来的浪费。
>由于lastTwo()只是fib()的一个实现细节,所以最好是把lastTwo()嵌入fib()中,
>以避免引入一个额外的函数名而使外层命名空间受到污染。
***
## 闭包
>函数可以定义在表达式的内部。它们被称为函数字面量,或者被更笼统地称为闭包。
>与函数声明不同,闭包没有名称。
>与其他函数一样,它们也有参数列表与函数体。
```dart
(x) => x; //另一个identity 函数
(x) { return x;} //又一个
(x, [step = 1]) => x + step; //有一个可选参数的闭包
(a, b) => a + b; //有两个必填参数的闭包
它们看起来就像是没有名称的函数声明。 真正的好处则来自于将它们作为大型表达式的一部分。
虑列表元素求和的例子 可以写一个for 循环来做到这一点,但那是相当原始的方法。 一个有经验的Dart 开发者会这样写:
sum(nums) => nums.reduce((a, b) => a + b);
Dart 中的列表及其他很多类型都定义了reduce()方法。 任意实现了lterable 接口的类型应该都有一个可运行的reduce()方法。 这个方法接收一个被我们称为combiner 的二元函数作为参数。 当reduce 被调用时,它会遍历当前的对象。 处理开始时,前两个元素将传递给combiner 并执行。 在后续的每个迭代中, combiner 被重新执行,上一次combiner 的执行结果会作为第1 个参数,而下一个元素将作为第2 个参数。>如果combiner()把它的参数相加,则最终效果就是把当前对象的前两个元素相,然后加上第3 个,以此类推,得到总和。
country.cities.where((city) =>city.population> 1000000);
这个函数大致找出了所有country 中人口数量大千一百万的城市。 这段代码使用了where()方法,它也来自于lterable, 接收一个函数作为参数。 在这种情况下, where()的参数必须是一个一元断言函数,而它将返回那些断言为真的元素。
函数可以通过标准的方式来调用,即在函数值表达式后面加上一个括号参数列表, 例如:
print('Hello, oh brave new world that has such people in it' ) ```。 有些函数被称为getter, 可以不使用参数列表来调用。 ```dart true.runtimeType; // bool
对getter 和setter 的统一使用为我们提供了表征独立的宝贵财富。 但需要注意,不像支持表征独立的其他几种语言, Dart 并不完全遵循统一引用的原则。 在Dart 中,方法跟字段不是通过相同的语法来访问的。 尽管getter 和字段没有区别对待,保证了表征独立,但getter 与方法是通过不同的语法访问的。 因此, Dart 开发者需要注意一个函数是被声明为getter 还是一个无参数的方法。这种情况同样适用千setter 。
除了平常使用点运算符来执行成员选择, Dart 也支持使用双点运算符进行方法级联。 当我们需要对一个对象执行一系列的操作时,级联是非常有用的。
级联的求值过程就像普通的方法调用,只是它的值不是方法调用的返回值,而是当前对象。
"Hello".length.toString(); // 值为'5' "Hello"..length; //值为 Hello 对象 "Hello"..length.toString(); //值为“Hello"
因此,我们并不这样写:
var address= new Address.of("Freddy Krueger"); address.setStreet ("Elm", "13a"); address.city = "Carthage"; address.state = "Eurasia"; address.zipCode(66666, extend: 66666); address;
我们可以写成一个单独的表达式:
new Address.of("Freddy Krueger") ..setStreet("Elm", "13a") ..city = "Carthage" ..state = "Eurasia" ..zipCode(66666, extend: 66666);
级联不仅仅节省了几个按键,还使得在没有预先进行规划的情况下,也能创造出流畅的API 。 没有级联时,为了将调用链接起来,所有方法必须设计为总是返回当前对象。 有了级联时,我们可以达成同样的链接效果,而且不用理会API 中各个方法的返回值。
将级联代码格式化以使它具有良好的可读性是很重要的。 我们不应该滥用级联,例如把一系列的级联放在同一行或是使用过多层次的级联。
级联对构建所谓建造者模式API 是非常有用的,即一个对象描述符被分步创建,在经历一系列级联后,对象在构建结束时才被创建。
另一种能用到级联的情况是当我们执行某个对象的方法时,我们需要的返回值是对象本身,但方法返回的却是其他值。 如果这样写:
var sortedColors = ['red', 'green', 'blue', 'orange', 'pink'].sublist(1, 3).sort();
则我们将发现sortedColors 是null, 因为sort()方法的返回值是void 。 我们可以重新调整代码:
var colors = ['red', 'green', 'blue', 'orange', 'pink'].sublist(1, 3); colors.sort();
但是直接使用级联是更好的办法:
var sortedColors = ['red', 'green', 'blue', 'orange', 'pink'].sublist(1, 3) ..sort();
Dart 中的赋值通常都是函数调用,因为对字段的赋值只是setter 方法的语法糖而已。
像
V = e
这样的赋值操作,其确切含义取决于v 的声明。如果v 是局部变量或参数,那么这就只是一个传统的赋值。 否则,这个赋值只是对调用名为
v=
的setter 的语法糖而已。赋值是否有效取决于 setter v 是否被定义,或变量v 是否为final 。 Final 变批不能重复赋值且不会导致对应的setter 被执行。
复合赋值如
i+= 2
被定义为普通的赋值
Dart 支待用户自定义的运算符,比如我们为Point 定义的
+
运算符。 Dart 中用户自定义的运算符实际上是有着特殊名称及特殊语法的实例方法,这些方法必须使用内置标识符operator
作为前缀。除了语法,所有实例方法涉及的规则都适用于运算符。
允许自定义的运算符有:
<、>、<=、>=、=、一、十、I 、~/、*、%、|、^、&、<<、>>、[J=、[]、~
。此外,还有一些固定的运算符不允许开发者自定义。它们是
&&
、||
及自增和自减运算符++
.--
(包括作为前缀与后缀)。赋值并不认为是一个运算符,虽然复合赋值的语义会依赖于运算符。
相同也不是Dart 的运算符。相反, Dart 提供了预定义的identical()函数。
运算符的优先级规则是固定且遵循惯例的,它们的元数也是一样的。
大多数运算符都是二元的。值得注意的例外是负号和
[]=
,后者是用于给类似数组和map (或任意有序集合)赋值的情况,并且需要两个参数:一个索引和一个新的值。 减号的情况是特殊的,因为我们支持二元的减法和一元的负号操作。
Function 是代表所有函数的公共顶层接口的抽象类。Function 没有声明任何实例方法。 然而它声明了类方法
apply()
,此方法接收一个函数和一个参数列表,并使用提供的参数列表去调用传入的函数。apply()的签名是:
static apply(Function function,List positionalArguments,[Map<Symbol, dynamic>namedArguments])
apply()的形式参数是带有类型注解的。它需要一个被调用的函数和一个位置参数的列表(可能为空)。 命名参数可以通过一个名称与实际参数组成的map 来提供,且实际参数可以是任意类型的对象。 最后一个参数是可选的。 大部分函数不需要任何命名参数,所以只在需要时才提供它们是比较方便的。 apply()方法提供了一种使用动态确定的参数列表来调用函数的机制。通过它,我们可以处理在编译时参数列表数量不确定的情况。
面向对象编程的关键原则是对象的行为而不是对象的实现。 理想情况下,任意对象都应该能模仿其他对象。 例如, Proxy 的实例被设计为模仿任意对象的行为。
而函数是对象,我们应该也能够模仿它们的行为 函数最常见且重要的行为是被调用时所执行的操作,但函数调用是一个内置的操作 用Proxy 来模仿函数
// 存疑 var p = new Proxy ((x) => x*2); p (1); //我们希望得到2
原来函数的执行会转换成调用一个名为call()的特殊方法。 所有真正的函数都隐含支持一个签名跟函数本身一致的call()方法,它的作用是执行当前的函数。
在上面的例子中, p(1)实际上是p.call(1) 。当然,我们不能把p.call(1)看作p.call.call(1) ,那将造成无限递归。 因为Proxy 没有call 方法,所以noSuchMethod()被执行,将调用发送到代理目标。 此代理目标是一个函数,它有自己的call()方法。 任何声明了call()方法的类都被认为隐含实现了Function 类。 注意Function 没有声明call()方法。原因是没有特定的函数签名来声明: call()可以有不同个数的参数,而且可能会有或者没有拥有不同默认值的可选参数(位置参数或命名参数)。 所以, Fuction 真的没有通用的call()可以声明。也因为这样, Dart 在语言层面对call()方法进行了特殊处理。
Dart 是一门纯面向对象的语言,所以Dart 中所有运行时的值都是对象,包括函数
函数也支待所有在Object 中声明的方法。
在多数情况下,函数继承Object 的方法并保持不变。 至于函数的toStringO方法,它的实现有一定的自由度。它们通常会产生一个对当前函数的合理描述,其中可能会包含函数的签名。
因为
==
和hashCode
的实现通常都是继承而来的,所以两个函数只有在相同的情况下才会相等。 否则,这将难以做到,因为在一般概念下,函数的语义相等是不可判定的。
同样,局部函数的声明每次被新的动态作用域包含时,就会引入一个新的对象
局部函数increment
makeCounter() { var counter = 0; increment() => ++counter; return increment; }
每次对makeCounter()的调用都返回一个不同的increment 函数。 在这种情况下,很明显,我们不需要相同的函数对象,因为它们每个都要绑定到一个不同的计数器变量。
然而,我们可以在某些情况下做得更好。 Dart 对通过对象属性获取的闭包给予了特殊对待。 如果(且仅当!)两个表达式o1 和o2 计算得到的是同一个对象(即ol 和o2 相同), 那么Dart 会确保对于给定的标识符m, 如果ol.m 是合法的,则ol.m==o2.m 。
noSuchMethod()的实现继承自Object, runt皿eType 同样如此。 Dart 运行时的各种类都可以用来表示函数。 所有这些类都将实现Function, 但对于不同函数的运行时类型,我们不能指望在它们之间执行相等与相同检测。
Dart 支待生成器,它是用来产生集合值的函数。 生成器可以是同步或异步的。 同步的生成器为迭代器生成提供语法糖 而异步的生成器则为流的生成提供语法糖。
迭代器是允许对集合内容按顺序进行迭代的对象。 在我们想简单生成集合内容时,迭代器特别方便。
支持通过迭代器进行迭代的集合被称为可迭代对象,可迭代对象必须有一个名为iterator 的用于返回迭代器的getter 。
for-in
循环可以操作任意可迭代对象。 迭代器与可迭代对象的接口分别被类Iterator 和Iterable 实现。 迭代器的生成非常公式化。我们需要定义一个可迭代的集合类且必须为它定义一个返回(明显地) iterator 的getter。 自然,你将需要定义一个具有moveNextO方法的迭代器类。作为例子,这里有一段使用令人沮丧的方式来打印到20 为止的自然数的代码: 示例,不能运行
class Naturalsiterable { var n; Naturalsiterable.to(this.n); get iterator => new Naturallterator(n); }
class Naturallterator { var n; var current = -1; Naturallterator(this.n); moveNext() { if (current < n) { current++; return true; } return false; } }
naturalsTo(n) => new Naturalsiterable.to(n); main() { for (var i in naturalsTo(20)) { print(i); } }
>实际上,一个实现了完整Iterable 接口的典型例子将会更长。
### 同步生成器
>为了减少迭代器而导致的重复代码, Dart 支持同步生成器函数。
>使用同步生成器,让我们省去了即使是实现最基本的迭代器也需要定义两个类的麻烦。
>我们可以给函数体使用```sync*```修饰符来定义生成器函数:
```dart
naturalsTo(n) sync* {
var k = 0;
while (k < n) yield k++;
}
被调用时,此函数将立即返回一个可迭代对象i, 该对象又包含了迭代器j。 在迭代器j的moveNext()第1 次被调用时,此函数才开始执行。 在进入循环后, yield 语句被执行,导致k 被加1 ,而上一次k 的值被追加到i 同时naturalsTo()的执行将暂停。 在下一次moveNext()被调用时,暂停yield 的naturalsTo()将继续执行同时循环将重复。
有更好的方式来实现这个特殊的例子。 当序列中的元素是基千自然数计算得来时,有一个方便的构造函数, lterable.generate()接收一个整数n 和一个函数f, 它将产生一个代表f(0)... f(n-1) 的可迭代对象:
naturalsTo (n) => new Iterable. generate (n, (x) => x);
虽然如此,但我们的例子是为了展示在通常情况下定义迭代器所需的必备要素,以及sync*是如何简化这些操作的。
生成器内return会直接终止生成器 在某些情况下, finally 分句可能会改变控制流程,进而导致return 不会终止生成器。
在生成器中我们不能使用return 语句来返回值,这样的语句将被编译器标记为错误。 允许这样的语句是没有任何意义的,因为调用者已经获得了返回值,且调用者已完成处理并从调用堆栈上消失。
虽然生成器在返回结果给调用者之后才运行,但它的函数体与结果仍有关联且会进行交互。 在同步生成器中,返回的结果始终是一个可迭代对象。 生成器始终跟它生成的可迭代对象及迭代器相关联。 同步生成器中的Yield 语句将对象追加到与之关联的可迭代对象,并暂停函数体的执行,如上所示。 只有通过调用与之关联的迭代器的moveNext()方法才能让函数体再次执行。 当yield 暂停执行函数体时, moveNext()返回true 给调用者。当生成器终止时, moveNext()返回false 。
函数作为一等值在编程语言中有着庞大而辉煌的历史。 函数式编程语言使用函数值作为基础构建模块
将函数作为对象的概念可以追溯到Smalltalk,它的block 是Dart 闭包的前身。 Smalltalk初始的block 存在较多的限制,它们在各种现代方言中都已被去除。
Dart 的函数与它在Smalltalk 语言中的祖先间的关键差异是闭包中return 的行为。 Smalltalk 支待非本地返回,这意味着在闭包中执行的return 将使外层方法退出并将结果返回给调用者。其结果是,使用接收函数的库函数来定义控制结构成为可能。 而这在Dart 中是不行的。不支持非本地返回这一决定,是底层Web 平台的实现限制所造成的。
Scala也支待非本地返回。
与其他所有运行时的值一样, Dart 函数是对象。 Dart 中的函数可以声明为接收位置或命名参数。 位置参数可以是必填或可选的,命名参数始终是可选的。
函数始终遵循词法作用域且对周边环境是封闭的。 然而,因为return 的语义, Dart 的函数不太适合用来实现用户自定义的控制结构。
Dart 函数既能作为类的方法也能作为独立的结构。方法可以与实例(实例方法)或类(类方法)关联。 独立的函数可以在库级别进行声明(顶层函数),也可以通过函数声明或字面表达式成为其他函数内的本地函数。
所有内置的操作符也都是函数,且它们大部分都被定义为实例方法,可以被开发者重写。 用户定义的类可以通过实现特殊的call 方法来模拟内置函数类型。 所有Dart 函数都被认为是Function 类型的成员。
Dart 的类型是可选的
一门语言之所以被称为类型可选,仅当以下条件成立时: • 类型在语法层面是可选的; • 类型对运行时语义没有影响。
后者比前者更加重要。 习惯了传统静态类型语言的开发者起初可能为此感到不安。 这一点虽然不起眼却是至关重要的,它是Dart 语言设计的基石。
首先,它是Dart 能作为动态语言使用的关键因素;任意用动态语言编写的程序都应当可以用Dart 实现。我们预想代码是会演变的,会随着时间推移而获得类型注解。 如果类型注解会改变Dart 程序的行为,则意味着把类型注解添加到程序中,很有可能会使正常运行的程序停止工作。 这将使类型注解的使用得不到推广,因为开发者们害怕正常工作的代码会因此出现故障。
此外, Dart 程序往往会同时包含使用了类型和没有使用类型的代码。 这意味着开发者不能假定类型安全,而且无法假定某个类型注解的正确性。 在这种情况下,允许类型注解来承担语义可能会造成混乱及不稳定。
基于类型的重载即使在全静态语言中也是一个存在问题的功能。 因为类型不影响语义,所以Dart 不支持基于类型的重载。 因此,即使我们给前面列出的所有例子都添加类型注解,它们在生产环境下的表现也不会发生改变。
Dart 的变量可以与类型关联。 类型也可以用来指示方法的返回类型
int sum(int a, int b) => a + b; void main() { print(sum(3, 4)); }
阅读代码的人能够受益于类型注解提供的文档, 但Dart 运行时对此并不关心。
开发工具如集成开发环境(IDE) 可以通过不同的方式来利用类型注解: 它们可以对可能存在的类型不一致发出警告,可以通过多种方式来帮助开发者, 例如为当前表达式提供适用的方法菜单(自动补全),或者提供基千类型信息的代码重构等。
Object 是所有不同类型的公共父类型,所以把参数标记为Object 类型看起来是合理的。但是事实并非如此。 显式使用类型Object, 意味着我们真正期望此变量中的每个对象都必须是 有效的值。这两种情况看起来类似,但当我们对Object 类型的表达式进行操作时,如果尝 试使用不被所有对象支持的方法,则我们会得到警告
如果程序中的变量没有显式地给予类型,则它的类型就是dynamic 。 dynamic 类型是一种特殊的类型,它告知类型检查器不要对变量本身的操作或给变量赋值等行为发出警告。 使用类型dynamic 能有效地使静态类型检查安静下来。它告诉类型检查器,我们明确知道自己在做什么。 在很难(或者无法)找到确切的类型来描述代码逻辑的情况下,这是非常有用的。
原则上来说,我们可以显示地使用dynamic 作为类型注解。
dynamic sum(dynamic a, dynamic b) => a + b; //永远不要这样做!
这是毫无意义的,而且是一种糟糕的风格。 它没有传达任何信息给类型检查器或读者。 不添加类型注解可以达到同样的效果,而且不会带来不必要的混乱。
import 'dart:math';
class Point {
num x, y;
Point(this.x, this.y);
Point scale(num factor) => new Point(x * factor, y * factor);
Point operator +(Point p) => new Point(x + p.x, y + p.y);
static num distance(Point p1, Point p2) {
num dx = p1.x - p2.x;
num dy = p1.y - p2.y;
return sqrt(dx * dx + dy * dy);
}
}
选择num 是因为它是整数与浮点数的公共父类型
在使用了初始化形式参数的构造函数简写中,并没有用到类型注解。 参数的类型可以由实例变量的声明处获得,因此没有必要重复。
Dart 的类型是接口类型。 它们给对象定义了一组可用的方法。 一般来说,它们不会告诉我们对象的具体实现。 这与关注对象的行为而非实现的基本原则是一致的。
Dart 并没有声明接口的语法。接口是通过类声明引入的。 每个类都引入了一个隐性接口,其签名是基于类的成员。 对于传统接口声明的需求,我们定义一个纯抽象类就可以轻松解决。
任何类都可以实现一个接口,即使该接口与类完全没有关联。 这是Dart 不需要接口声明语法的原因。
Pair:
abstract class Pair { get first; get second; }
以声明一个类,它实现了Pair 所定义的接口:
class ArrayPair implements Pair { var _rep; ArrayPair(a, b) { _rep = [a, b]; } get first => _rep[0]; get second => _rep[1]; }
类ArrayPair 实现了Pair, 而不是继承它。implements 子句后跟着一个或多个类想要实现的接口。
implements 子句所做的,是使类与它所列出的接口建立明确的子类关系。 这种关系会影响Dart 类型检查器及运行时的行为。
一个操作Pair 类对象的函数:
Pair reversePair(Pair p) => new ArrayPair(p.second, p.first);
使用reversePair:
reversePair(new ArrayPair(3, 4)); //一个新的Pair对象,first = 4 且second = 3
当对象从一个变量传递到另一个变量时,会触发类型检查。这样的值传递发生千: • 执行赋值操作; • 传递实际参数给函数; • 函数返回结果。
implements 子句中列出的每个接口都被认为是当前类的直接父接口。
类的父类也被认为是类的直接父接口之一。
如果将类型的所有父接口看作一个集合,那么我们可以这样来计算它:先取得类型的所有直接父接口,然后递归计算它们的直接父接口。 计算一直进行,直到没有元素可以被添加到集合中。
严格地说,在值传递的过程中,类型检查器并不强制要求变量之间有父接口关系。 它检查的是可赋值性。可赋值性比继承关系更宽松。 只要两个类型之间存在父子关系, Dart就认为它们可以相互赋值。 也就是说,不仅ArrayPair 可以赋给Pair (因为ArrayPair 是Pair的子类型), Pair也可以赋给ArrayPair。
Dart 的可赋值性规则支待类型隐性向下转换。
着Dart 的类型准则是弱类型的。我们不能保证通过类型检查的Dart 程序在运行时不会出错。 这种保证在现实中没有哪种静态类型系统可以做到。
通常来说,类型系统能够强制的属性都会被定义为类型准则的一部分,而对于不能强制的属性就只能有不同程度的降级处理。>一个典型的例子是函数式语言中的模式匹配,它所引入某些属性是不属于静态类型系统范畴的。
Dart 的类可以是泛化的,也就是说,它们能通过类型进行参数设置。
泛型类可以指定实际的类型参数:
List<String> L = []; Map<String, int> m = {};
给泛型类提供类型参数并不是必需的
如果我们选择使用泛型类且不提供类型参数,则类型dynamic 将会被隐性使用,代替所有缺失的类型参数
Map<String, dynamic> mm = {};
虽然类型注解不会出现在运行时,但类型的其他方面会。
每个对象都承载了自身的运行时类型,而且可以通过由Object 继承而来的runtimeType 方法进行访问。 用户可以自由地重写runtimeType, 这也意味着在一般情况下,对象的实现类型并不能通过调用nmtimeType 来获得。
每个类型声明都会引入一个代表它自身的类型为Type 的编译时常量对象。 这些对象在运行时也是可见的。 可以通过动态类型检测和强制类型转换来测试对象是否为某个类型的成员
类型检测是用来测试对象是否属千某个类型的表达式:
var v = [1, 2, 3]; v is List; // true v is Map; // false v is Object; //无意义的:始终为true
类型检测的一般形式是e is T, 其中e 是一个表达式而T 是一个类型。 类型检测会对e求值并将结果的动态类型与类型T 做对比测试。
上述代码的最后一行是永远不能出现在正常的Dart 程序中的。它的值始终是true, 因为Dart 中所有的值都是对象。
强制类型转换同样是对一个表达式求值并测试结果对象是否属于某个类型, 不同的是,它们的结果并不明确。 相反,如果测试失败,则强制类型转换会抛出一个CastError, 否则它将返回未改动的被检测的对象。
Object o = [3, 4, 5]; o as List; //有点昂贵的空操作 o as Map; //抛出异常
var t = e;
t is T ? t : throw new CastError();
List L = readNextVal() as List;
//我确信我从readNextVal( )中得到的是一个列表
//如果不是,事情搞砸了,那么我应该失败
//接下来,对列表进行处理
Object o = [5, 6, 7];
// 大量的中间逻辑
o.length;//正常工作,但会产生类型警告:因为不是所有对象都有length 属性
许多开发者可能会试图将以上代码改写为;
Object o = [5, 6, 7]; //相同的中间逻辑 (o as List).length; //糟糕!避免警告的错误方式
强制类型转换是在运行时执行的,因此会带来运行时消耗。 如果你的目的只是让类型检查器安静,那么只需要一个赋值语句: 存疑
Object O = [5, 6, 7]; //相同的中间逻样 List L = O; L.length;
在开发过程中,对变量的类型进行校验是非常有用的。 例如,我们想确保输入的参数或返回的对象符合我们的预期。 Dart 为此提供了检查模式。 在检查模式中,每次发生值传递都会触发动态检查。这意味着,在每次的参数传递中,函数或方法返回结果以赋值操作时, Dart 都自动执行一次动态类型测试。 检查模式确保了赋给变量的动态值是变量的静态类型的成员。 同样,实际参数的动态类型也会与形式参数的静态类型进行对比检测, 而函数结果的动态类型则会与函数声明的返回类型进行对比检测。
num n = 3.0; int i = n; //梒查模式下将触发动态错误;,生产模式下正常工作 num x = i; //始终正常工作 int j = null;
检查模式的行为不同于静态类型检查规则。 当赋值被执行静态检查时,我们使用可指派性规则,即只要双方存在子类或父类关系就允许赋值。 而在检查模式所实现的动态检查中,赋值操作中的值的真实类型必须是变量的静态类型的子类,或者是null 。
在没有检查模式的情况下,开发者可以为代码添加类型转换,但那样会令人不快。 类型转换不仅烦琐,还会引入运行时开销,在生产环境下全面使用类型转换的代价是很高的, 所以只有真正需要的时候才使用类型转换。
在检查模式下,类型注解是会影响程序的行为,但是检查模式也可看作一个完全受开发者控制的为验证类型注解正确性的工具。 在检查模式下,类型注解非常类似千断言。检查模式同时会激活程序中所有的assert 语句
在运行时,类型参数是具体化的。 当泛型类被实例化时,在运行时传递与储存的都是实际的类型参数。 因此,
new List<String>()
创建的实例的类实际上是不同于newList<Object>()
所创建实例的类的。 我们可以编写如下代码进行测试:
var L = new List<String>();
L is List<String>; // true
L is List<int>; // false
Dart 中的这种测试是存在局限性的。因为类型系统是不严格的,所以我们不能保证某个对象一定符合它的声明, 比如一个
List<int>
里面只包含整数。我们可以确信,该对象是一个列表,并且它被创建为整数列表。但是,任何类型的对象随后都可以被插入到这个列表中。但在检查模式下,我们可以更加自信。通过插入不合适的对象从而试图破坏该列表的行为都将被检查模式阻止。 检查模式会将对象的实际类型与变量声明的类型和函数结果的类型进行比较和测试: 而在泛型类型中,泛型的实际类型参数将被类型声明中的类型变量所替代。
var L = new List<String>(); L[0] = 'abc'; //始终ok L[1] = 42; //检查模式下无法运行- 42 是整型,不是String 的子类型
检查模式将确保泛型类的实例的使用不会被破坏。
虽然可选类型应该不会影响运行时语义,但泛型的具体化肯定会影响运行时语义。 虽然如此,具体化也只在程序试图观察或确定运行时类型结构时才会影响程序的行为。 这些情况包括: • 使用类型测试,强制转换或调用runtimeType 来查询对象的类型; • 给泛型类型的构造函数传递实际类型参数以设置对象的运行时类型; • 使用反射检查或设置变量或函数的类型; • 使用检查模式; • 通过给函数对象的签名添加类型注解来确定其具体化类型。
最后三点都有可能受到类型注解的影响,但一般不会产生语义效果。 其中,最后一点可能是最微妙的。如果类型测试的逻辑牵涉函数,则它会被类型注解影响。 这个函数可以是一个明确定义的闭包或被提取为对象属性
下面的代码演示了该问题:
typedef int IntFunction(int);
observeAnnotations(f) { return f is IntFunction; }
String idl(String x) => x;
id2(x) => x;
int id3(int x) => x;
main() { observeAnnotations(idl); // false observeAnnotations(id2); // true observeAnnotations(id3); // true }
>除了类型注解外,三个函数idl 、id2 、id3 是一模一样的,也因为使用了不同的类型注解,所以导致它们在类型测试中的表现不一致。
>当然,我们这里明确地执行了一个类型测试,所以这种情形下的类型影响行为的表现并不会让我们感到惊讶。
### 类型和代理
>
Dart 是谷歌开发的计算机编程语言,后来被ECMA CECMA-408) 认定为标准。 它被用于Web 、服务器移动应用和物联网等领域的开发。 它是宽松开源许可证(修改的BSD 证书)下的开源软件。 Dart 有以下三个方向的用途,每一个方向,都有相应的SDK 。 Dart 语言可以创建移动应用、Web 应用,以及Command-line 应用等
Dart 在移动端上的应用离不开Flutter 技术
Flutter 采用Dart 的原因很多.单纯从技术层面分析如下:
Dart 是AOT(Ahead Of Time) 编译的,可编译成快速、可预测的本地代码, Flutter几乎可以使用Dart 编写; Dart 也可以JIT(J ust In Time) 编译,开发周期快; Dart 可以更轻松地创建以60fps 运行的流畅动画和转场; Dart 使Flutter 不需要单独的声明式布局语言; Dart 容易学习,具有静态和动态语言用户都熟悉的特性。 Dart 最初设计是为了取代JavaScript 成为Web 开发的首选语言,最后的结果可想而知,因此到Dart 2 发布时,巳专注于改善构建客户端应用程序的体验,可以乔出Dart 定位的转变。用过Java 、Kotlm 的人,可以很快地上手Dart 。
Dart 是经过关键性Web 应用程序验证的平台。它拥有为Web 蚊身打造的库,如
dart:html
,以及完整的基于Dart 的Web 框架。使用Dart 进行Web 开发的团队会对速度的提 高感到非常激动。选择Dart 是因为其高性能、可预测性和易学性、完善的类型系统,以及完美地支持Web 和移动应用。
Dart 的服务端开发与其他的语言类似,有完整的库,可以帮助开发者快速开发服务端代码。
Dart 的关键字
abstract2 | dynamic2 | implements2 | show1 |
---|---|---|---|
as2 | else | import2 | static2 |
assert | enum | in | super |
async1 | export2 | in2 | super |
await3 | extends | is | sync1 |
break | external2 | library2 | this |
case | factory2 | mixin2 | throw |
catch | false | new | true |
class | final | null | try |
const | finally | on1 | typedef2 |
continue | for | operator2 | var |
covariant2 | Function2 | part2 | void |
default | get2 | rethrow | while |
deferred2 | hide1 | return | with |
do | if | set2 | yield3 |
Dart
Dart 开发语言概览
一个简单的 Dart 程序
重要概念
关键字
变量 Variables
默认值
final 和 const
使用关键字 const 修饰变量表示该变量为 编译时常量。
内置类型
num
int
整型字面量将会在必要的时候自动转换成浮点数字面量:
整型支持传统的位移操作
字符串和数字之间转换的方式:
数字字面量为编译时常量
字符串 String
${表达式}
assert('Dart has $s, which is very handy.' == 'Dart has string interpolation, ' + 'which is very handy.'); assert('That deserves all caps. ' + '${s.toUpperCase()} is very handy!' == 'That deserves all caps. ' + 'STRING INTERPOLATION is very handy!');
“raw” 字符串
// 代码中文解释 var s = r'在 raw 字符串中,转义字符串 \n 会直接输出 “\n” 而不是转义为换行。';
bool
应该总是显示地检查布尔值
// 检查是否小于等于零。 var hitPoints = 0; assert(hitPoints <= 0);
// 检查是否为 null。 var unicorn; assert(unicorn == null);
// 检查是否为 NaN。 var iMeantToDoThis = 0 / 0; assert(iMeantToDoThis.isNaN);
可以在 Dart 的集合类型的最后一个项目后添加逗号
list[1] = 1; assert(list[1] == 1);
扩展操作符
...
和 空感知扩展操作符...?
可以使用扩展操作符
...
将一个 List 中的所有元素插入到另一个 List 中:如果扩展操作符右边可能为 null ,你可以使用 null-aware 扩展操作符
...?
来避免产生异常:集合中的 if 和 集合中的 for 操作
Set
可以使用在
{}
前加上类型参数的方式创建一个空的 Set,或者将{}
赋值给一个 Set 类型的变量:使用
.length
可以获取 Set 中元素的数量:可以在 Set 字面量前添加 const 关键字创建一个 Set 编译时常量:
从 Dart 2.3 开始,Set 可以像 List 一样支持使用扩展操作符
...
和...?
以及 Collection if 和 for 操作Map
var nobleGases = { 2: 'helium', 10: 'neon', 18: 'argon', };
向现有的 Map 中添加键值对与 JavaScript 的操作类似:
如果检索的 Key 不存在于 Map 中则会返回一个 null:
使用 .length 可以获取 Map 中键值对的数量:
在一个 Map 字面量前添加 const 关键字可以创建一个 Map 编译时常量:
Map 可以像 List 一样支持使用扩展操作符
...
和...?
以及集合的 if 和 for 操作。runes 与 grapheme clusters
Symbol
函数 Function
参数 Parameter
命名参数
可选的位置参数
默认参数值
设置可选参数默认值示例:
为位置参数设置默认值:
List 或 Map 同样也可以作为默认值
main()
函数下面是一个简单 main() 函数:
使用命令行访问带参数的
main()
函数示例:函数是一级对象
可以将函数作为参数传递给另一个函数
可以将函数赋值给一个变量
匿名函数
如果函数体内只有一行返回语句,你可以使用胖箭头缩写法。
词法作用域
void main() { var insideMain = true;
void myFunction() { var insideFunction = true;
} }
测试函数是否相等
class A { static void bar() {} // 定义静态方法 void baz() {} // 定义实例方法 }
void main() { Function x;
// 比较顶层函数是否相等。 x = foo; assert(foo == x);
// 比较静态方法是否相等。 x = A.bar; assert(A.bar == x);
// 比较实例方法是否相等。 var v = A(); // A 的实例 #1 var w = A(); // A 的实例 #2 var y = w; x = w.baz;
// 这两个闭包引用了相同的实例对象,因此它们相等。 assert(y.baz == x);
// 这两个闭包引用了不同的实例对象,因此它们不相等。 assert(v.baz != w.baz); }
运算符 Operator
// 难以理解,但是与上面的代码效果一样。 if (n % i == 0 && d % i == 0) ...
var a, b;
a = 0; b = ++a; // 在 b 赋值前将 a 增加 1。 assert(a == b); // 1 == 1
a = 0; b = a++; // 在 b 赋值后将 a 增加 1。 assert(a != b); // 1 != 0
a = 0; b = --a; // 在 b 赋值前将 a 减少 1。 assert(a == b); // -1 == -1
a = 0; b = a--; // 在 b 赋值后将 a 减少 1。 assert(a != b); // -1 != 0
赋值运算符
逻辑运算符
按位和移位运算符
assert((value & bitmask) == 0x02); // 按位与 (AND) assert((value & ~bitmask) == 0x20); // 取反后按位与 (AND NOT) assert((value | bitmask) == 0x2f); // 按位或 (OR) assert((value ^ bitmask) == 0x2d); // 按位异或 (XOR) assert((value << 4) == 0x220); // 位左移 (Shift left) assert((value >> 4) == 0x02); // 位右移 (Shift right)
// Very long version uses if-else statement. String playerName(String? name) { if (name != null) { return name; } else { return 'Guest'; } }
级联运算符可以嵌套
在返回对象的函数中谨慎使用级联操作符。
其他运算符
流程控制语句
If 和 Else
For 循环
在 Dart 语言中,for 循环中的闭包会自动捕获循环的 索引值 以避免 JavaScript 中一些常见的陷阱。
如果要遍历的对象是一个可迭代对象(例如 List 或 Set),并且你不需要知道当前的遍历索引,则可以使用 for-in 方法进行 遍历:
While 和 Do-While
while 循环会在执行循环体前先判断条件:
do-while
循环则会 先执行一遍循环体 再判断条件:Break 和 Continue
Switch 和 Case
不匹配任何 case 语句的情况下,会执行 default 子句中的代码:
忽略了 case 子句的 break 语句,会产生错误:
但是,Dart 支持空的 case 语句,允许其以
fall-through
的形式执行。在非空 case 语句中想要实现
fall-through
的形式,可以使用 continue 语句配合 label 的方式实现:断言 assert
assert 的第二个参数可以为其添加一个字符串消息。
异常 Exception
抛出异常
捕获异常
void main() { try { misbehave(); } catch (e) { print('main() finished handling ${e.runtimeType}.'); } }
类 class
使用类的成员
// 获取 y 值 assert(p.y == 2);
// 调用变量 p 的 distanceTo() 方法。 double distance = p.distanceTo(Point(4, 4));
使用构造函数 constructor
assert(identical(a, b)); // 它们是同一个实例 (They are the same instance!)
assert(!identical(a, b)); // 这两变量并不相同 (NOT the same instance!)
实例变量
void main() { var point = Point(); point.x = 4; // 使用 x 的 Setter 方法。 assert(point.x == 4); // 使用 x 的 Getter 方法。 assert(point.y == null); // 默认值为 null。 }
构造函数
Point(double x, double y) { // 还会有更好的方式来实现此逻辑,敬请期待。 this.x = x; this.y = y; } }
默认构造函数
构造函数不被继承
命名式构造函数
可以为一个类声明多个命名式构造函数来表达更明确的意图:
调用父类非默认构造函数
Person.fromJson(Map data) { print('in Person'); } }
class Employee extends Person { // Person does not have a default constructor; // you must call super.fromJson(data). Employee.fromJson(Map data) : super.fromJson(data) { print('in Employee'); } }
void main() { var employee = Employee.fromJson({}); print(employee); // Prints: // in Person // in Employee // Instance of 'Employee' }
初始化列表
class Point { final double x; final double y; final double distanceFromOrigin;
Point(double x, double y) : x = x, y = y, distanceFromOrigin = sqrt(x x + y y); }
void main() { var p = Point(2, 3); print(p.distanceFromOrigin); }
常量构造函数
final double x, y;
const ImmutablePoint(this.x, this.y); }
var logMap = {'name': 'UI'}; var loggerJson = Logger.fromJson(logMap);
操作符
为了表示重写操作符,使用 operator 标识来进行标记。
Vector(this.x, this.y);
Vector operator +(Vector v) => Vector(x + v.x, y + v.y); Vector operator -(Vector v) => Vector(x - v.x, y - v.y);
// Operator == and hashCode not shown. // ··· }
void main() { final v = Vector(2, 3); final w = Vector(2, 2);
assert(v + w == Vector(4, 5)); assert(v - w == Vector(0, 1)); }
抽象方法
void doSomething(); // 定义一个抽象方法。 }
class EffectiveDoer extends Doer { void doSomething() { // 提供一个实现,所以在这里该方法不再是抽象的…… } }
隐式接口 implements
// 构造函数不在接口中。 Person(this._name);
// greet() 方法在接口中。 String greet(String who) => '你好,$who。我是$_name。'; }
// Person 接口的一个实现。 class Impostor implements Person { String get _name => '';
String greet(String who) => '你好$who。你知道我是谁吗?'; }
String greetBob(Person person) => person.greet('小芳');
void main() { print(greetBob(Person('小芸'))); print(greetBob(Impostor())); }
扩展一个类 extends
class SmartTelevision extends Television { void turnOn() { super.turnOn(); _bootNetworkInterface(); _initializeMemory(); _upgradeApps(); } // ··· }
covariant
class Mouse extends Animal { ... }
class Cat extends Animal { @override void chase(covariant Mouse x) { ... } }
只有下面其中一个条件成立时,你才能调用一个未实现的方法:
扩展方法
print('42'.padLeft(5)); // Use a String method. print('42'.parseInt()); // Use an extension method.
switch (aColor) { case Color.red: print('红如玫瑰!'); break; case Color.green: print('绿如草原!'); break; default: // 没有该语句会出现警告。 print(aColor); // 'Color.blue' }
实现一个 mixin
void entertainMe() { if (canPlayPiano) { print('Playing piano'); } else if (canConduct) { print('Waving hands'); } else { print('Humming to self'); } } }
类变量和方法 static
静态变量
void main() { assert(Queue.initialCapacity == 16); }
泛型 Generic
为什么使用泛型?
使用集合字面量
使用类型参数化的构造函数
泛型集合以及它们所包含的类型
限制参数化类型
<T extends SomeBaseClass>
class Extender extends SomeBaseClass {...}
使用泛型方法
function<T>
库和可见性
使用库 import
指定库前缀 as
// 使用 lib1 的 Element 类。 Element element1 = Element();
// 使用 lib2 的 Element 类。 lib2.Element element2 = lib2.Element();
延迟加载库 deferred as
实现库
异步支持
处理 Future
声明异步函数 async
处理 Stream
使用 await for 定义异步循环看起来是这样的:
使用 break 和 return 语句可以停止接收 Stream 数据,这样就跳出了循环并取消注册监听 Stream。
如果在实现异步 for 循环时遇到编译时错误,请检查确保 await for 处于异步函数中。
生成器 generator
可调用类 call
var wf = WannabeFunction(); var out = wf('你好', ',使用 Dart 的', '朋友');
void main() => print(out);
int sort(int a, int b) => a - b;
void main() { assert(sort is Compare); // True!
}
自定义元数据注解
class Todo { final String who; final String what;
const Todo(this.who, this.what); }
注释
单行注释
多行注释
Llama larry = Llama(); larry.feed(); larry.exercise(); larry.clean(); */ }
Dart 编程语言规范