Dart类
类
Dart 是支持基于 mixin 继承机制的面向对象语言,所有对象都是一个类的实例,而除了 Null
以外的所有的类都继承自 Object
类。 基于 mixin 的继承 意味着尽管每个类(top class Object?
除外)都只有一个超类,一个类的代码可以在其它多个类继承中重复使用。 扩展方法 是一种在不更改类或创建子类的情况下向类添加功能的方式。
使用类的成员
对象的 成员 由函数和数据(即 方法 和 实例变量)组成。方法的 调用 要通过对象来完成,这种方式可以访问对象的函数和数据。
使用(.
)来访问对象的实例变量或方法:
1 | var p = Point(2, 2); |
使用 ?.
代替 .
可以避免因为左边表达式为 null 而导致的问题:
1 | // If p is non-null, set a variable equal to its y value. |
使用构造函数
可以使用 构造函数 来创建一个对象。构造函数的命名方式可以为 *类名*
或 *类名* . *标识符*
的形式。例如下述代码分别使用 Point()
和 Point.fromJson()
两种构造器创建了 Point
对象:
1 | var p1 = Point(2, 2); |
以下代码具有相同的效果,但是构造函数名前面的的 new
关键字是可选的:
1 | var p1 = new Point(2, 2); |
一些类提供了常量构造函数。使用常量构造函数,在构造函数名之前加 const
关键字,来创建编译时常量时:
1 | var p = const ImmutablePoint(2, 2); |
两个使用相同构造函数相同参数值构造的编译时常量是同一个对象:
1 | var a = const ImmutablePoint(1, 1); |
在 常量上下文 场景中,你可以省略掉构造函数或字面量前的 const
关键字。例如下面的例子中我们创建了一个常量 Map:
1 | // Lots of const keywords here. |
根据上下文,你可以只保留第一个 const
关键字,其余的全部省略:
1 | // Only one const, which establishes the constant context. |
但是如果无法根据上下文判断是否可以省略 const
,则不能省略掉 const
关键字,否则将会创建一个 非常量对象 例如:
1 | var a = const ImmutablePoint(1, 1); // Creates a constant |
获取对象的类型
可以使用 Object
对象的 runtimeType
属性在运行时获取一个对象的类型,该对象类型是 Type
的实例。
1 | print('The type of a is ${a.runtimeType}'); |
实例变量
下面是声明实例变量的示例:
1 | class Point { |
所有未初始化的实例变量其值均为 null
。
所有实例变量均会隐式地声明一个 Getter 方法。非终值的实例变量和 late final
声明但未声明初始化的实例变量还会隐式地声明一个 Setter 方法。你可以查阅 Getter 和 Setter 获取更多相关信息。
1 | class Point { |
实例变量可以是final,在这种情况下,它们必须设置一次。使用构造函数参数或构造函数的初始值设定项列表在声明时初始化最终的非后期实例变量:
1 | class ProfileMark { |
如果需要在构造函数主体启动后分配最终实例变量的值,可以使用以下方法之一:
使用工厂构造函数。
使用晚期final,但要小心:没有初始值设定项的晚期final会向API添加setter。
构造函数
声明一个与类名一样的函数即可声明一个构造函数(对于命名式构造函数 还可以添加额外的标识符)。大部分的构造函数形式是生成式构造函数,其用于创建一个类的实例:
1 | class Point { |
使用 this
关键字引用当前实例。
当且仅当命名冲突时使用
this
关键字才有意义,否则 Dart 会忽略this
关键字。
终值初始化
对于大多数编程语言来说在构造函数中为实例变量赋值的过程都是类似的,而 Dart 则提供了一种特殊的语法糖来简化该步骤。
构造中初始化的参数可以用于初始化非空或 final
修饰的变量,它们都必须被初始化或提供一个默认值。
1 | class Point { |
在初始化时出现的变量默认是隐式终值,且只在初始化时可用。
默认构造函数
如果你没有声明构造函数,那么 Dart 会自动生成一个无参数的构造函数并且该构造函数会调用其父类的无参数构造方法。
构造函数不被继承
子类不会继承父类的构造函数,如果子类没有声明构造函数,那么只会有一个默认无参数的构造函数。
命名式构造函数
可以为一个类声明多个命名式构造函数来表达更明确的意图:
1 | const double xOrigin = 0; |
记住构造函数是不能被继承的,这将意味着子类不能继承父类的命名式构造函数,如果你想在子类中提供一个与父类命名构造函数名字一样的命名构造函数,则需要在子类中显式地声明。
调用父类非默认构造函数
默认情况下,子类的构造函数会调用父类的匿名无参数构造方法,并且该调用会在子类构造函数的函数体代码执行前,如果子类构造函数还有一个 初始化列表,那么该初始化列表会在调用父类的该构造函数之前被执行,总的来说,这三者的调用顺序如下:
- 初始化列表
- 父类的无参数构造函数
- 当前类的构造函数
如果父类没有匿名无参数构造函数,那么子类必须调用父类的其中一个构造函数,为子类的构造函数指定一个父类的构造函数只需在构造函数体前使用(:
)指定。
下面的示例中,Employee 类的构造函数调用了父类 Person 的命名构造函数。
1 | class Person { |
因为参数会在子类构造函数被执行前传递给父类的构造函数,因此该参数也可以是一个表达式,比如一个函数:
1 | class Employee extends Person { |
传递给父类构造函数的参数不能使用
this
关键字,因为在参数传递的这一步骤,子类构造函数尚未执行,子类的实例对象也就还未初始化,因此所有的实例成员都不能被访问,但是类成员可以。
超类参数
为了不重复地将参数传递到超类构造的指定参数,你可以使用超类参数,直接在子类的构造中使用超类构造的某个参数。超类参数不能和重定向的参数一起使用。超类参数的表达式和写法与 终值初始化 类似:
1 | class Vector2d { |
如果超类构造的位置参数已被使用,那么超类构造参数就不能再继续使用被占用的位置。但是超类构造参数可以始终是命名参数:
1 | class Vector2d { |
使用超类参数需要 Dart SDK 版本 至少为 2.17。在先前的版本中,你必须手动传递所有的超类构造参数。
初始化列表
除了调用父类构造函数之外,还可以在构造函数体执行之前初始化实例变量。每个实例变量之间使用逗号分隔。
1 | // Initializer list sets instance variables before |
初始化列表表达式 = 右边的语句不能使用
this
关键字。
在开发模式下,你可以在初始化列表中使用 assert
来验证输入数据:
1 | Point.withAssert(this.x, this.y) : assert(x >= 0) { |
使用初始化列表设置 final
字段非常方便,下面的示例中就使用初始化列表来设置了三个 final
变量的值。
1 | import 'dart:math'; |
重定向构造函数
有时候类中的构造函数仅用于调用类中其它的构造函数,此时该构造函数没有函数体,只需在函数签名后使用(:)指定需要重定向到的其它构造函数 (使用 this
而非类名):
1 | class Point { |
常量构造函数
如果类生成的对象都是不变的,可以在生成这些对象时就将其变为编译时常量。你可以在类的构造函数前加上 const
关键字并确保所有实例变量均为 final
来实现该功能。
1 | class ImmutablePoint { |
常量构造函数创建的实例并不总是常量,具体可以参考使用构造函数章节。
工厂构造函数
使用 factory
关键字标识类的构造函数将会令该构造函数变为工厂构造函数,这将意味着使用该构造函数构造类的实例时并非总是会返回新的实例对象。例如,工厂构造函数可能会从缓存中返回一个实例,或者返回一个子类型的实例。
另一种处理懒加载变量的方式是 使用
late final
(谨慎使用)。
在如下的示例中, Logger
的工厂构造函数从缓存中返回对象,和 Logger.fromJson
工厂构造函数从 JSON 对象中初始化一个最终变量。
1 | class Logger { |
在工厂构造函数中无法访问
this
。
工厂构造函数的调用方式与其他构造函数一样:
1 | var logger = Logger('UI'); |
方法
方法是为对象提供行为的函数。
实例方法
对象的实例方法可以访问实例变量和 this
。下面的 distanceTo()
方法就是一个实例方法的例子:
1 | import 'dart:math'; |
操作符
运算符是有着特殊名称的实例方法。 Dart 允许您使用以下名称定义运算符:
| <
| +
| |
| >>>
|
| —- | —- | —- | —– |
| >
| /
| ^
| []
|
| <=
| ~/
| &
| []=
|
| >=
| *
| <<
| ~
|
| -
| %
| >>
| ==
|
你可能注意到有一些 操作符 没有出现在列表中,例如
!=
。因为它们仅仅是语法糖。表达式e1 != e2
仅仅是!(e1 == e2)
的一个语法糖。
为了表示重写操作符,我们使用 operator
标识来进行标记。下面是重写 +
和 -
操作符的例子
1 | class Vector { |
Getter 和 Setter
Getter 和 Setter 是一对用来读写对象属性的特殊方法,上面说过实例对象的每一个属性都有一个隐式的 Getter 方法,如果为非 final 属性的话还会有一个 Setter 方法,你可以使用 get
和 set
关键字为额外的属性添加 Getter 和 Setter 方法:
1 | class Rectangle { |
使用 Getter 和 Setter 的好处是,你可以先使用你的实例变量,过一段时间过再将它们包裹成方法且不需要改动任何代码,即先定义后更改且不影响原有逻辑。
像自增(++)这样的操作符不管是否定义了 Getter 方法都会正确地执行。为了避免一些不必要的异常情况,运算符只会调用 Getter 一次,然后将其值存储在一个临时变量中。
抽象方法
实例方法、Getter 方法以及 Setter 方法都可以是抽象的,定义一个接口方法而不去做具体的实现让实现它的类去实现该方法,抽象方法只能存在于 抽象类中。
直接使用分号(;)替代方法体即可声明一个抽象方法:
1 | abstract class Doer { |
抽象类
使用关键字 abstract
标识类可以让该类成为 抽象类,抽象类将无法被实例化。抽象类常用于声明接口方法、有时也会有具体的方法实现。如果想让抽象类同时可被实例化,可以为其定义 工厂构造函数。
抽象类常常会包含 抽象方法。下面是一个声明具有抽象方法的抽象类示例:
1 | // This class is declared abstract and thus |
隐式接口
每一个类都隐式地定义了一个接口并实现了该接口,这个接口包含所有这个类的实例成员以及这个类所实现的其它接口。如果想要创建一个 A 类支持调用 B 类的 API 且不想继承 B 类,则可以实现 B 类的接口。
一个类可以通过关键字 implements
来实现一个或多个接口并实现每个接口定义的 API:
1 | // A person. The implicit interface contains greet(). |
如果需要实现多个类接口,可以使用逗号分割每个接口类:
1 | class Point implements Comparable, Location {...} |
扩展一个类
使用 extends
关键字来创建一个子类,并可使用 super
关键字引用一个父类:
1 | class Television { |
重写类成员
子类可以重写父类的实例方法(包括 操作符)、 Getter 以及 Setter 方法。你可以使用 @override
注解来表示你重写了一个成员:
1 | class Television { |
重写方法声明必须以多种方式匹配它重写的方法:
返回类型必须与重写方法的返回类型相同(或其子类型)。
参数类型必须与重写方法的参数类型相同(或其超类型)。在前面的示例中,SmartTelevision的对比度设置器将参数类型从int更改为超类型num。
如果重写方法接受n个位置参数,则重写方法也必须接受n个定位参数。
泛型方法不能重写非泛型方法,非泛型方法也不能重写泛型方法。
你可以使用 covariant
关键字 来缩小代码中那些符合 类型安全 的方法参数或实例变量的类型。
如果重写
==
操作符,必须同时重写对象hashCode
的 Getter 方法。你可以查阅 实现映射键 获取更多关于重写的==
和hashCode
的例子。
noSuchMethod 方法
如果调用了对象上不存在的方法或实例变量将会触发 noSuchMethod
方法,你可以重写 noSuchMethod
方法来追踪和记录这一行为:
1 | class A { |
只有下面其中一个条件成立时,你才能调用一个未实现的方法:
- 接收方是静态的
dynamic
类型。 - 接收方具有静态类型,定义了未实现的方法(抽象亦可),并且接收方的动态类型实现了
noSuchMethod
方法且具体的实现与Object
中的不同。
扩展方法
扩展方法是向现有库添加功能的一种方式。你可能已经在不知道它是扩展方法的情况下使用了它。例如,当您在 IDE 中使用代码完成功能时,它建议将扩展方法与常规方法一起使用。
这里是一个在 String
中使用扩展方法的样例,我们取名为 parseInt()
,它在 string_apis.dart
中定义:
1 | import 'string_apis.dart'; |
枚举类型
枚举类型是一种特殊的类型,也称为 enumerations 或 enums,用于定义一些固定数量的常量值。
所有的枚举都继承于
Enum
类。枚举类是封闭的,即不能被继承、被实现、被 mixin 混入或显式被实例化。抽象类和 mixin 可以显式的实现或继承
Enum
,但只有枚举可以实现或混入这个类,其他类无法享有同样的操作。
声明简单的枚举
你可以使用关键字 enum
来定义简单的枚举类型和枚举值:
1 | enum Color { red, green, blue } |
声明增强的枚举类型
Dart 中的枚举也支持定义字段、方法和常量构造,常量构造只能构造出已知数量的常量实例(已定义的枚举值)。
你可以使用与定义 类 类似的语句来定义增强的枚举,但是这样的定义有一些限制条件:
- 实例的字段必须是
final
,包括由 mixin 混入的字段。 - 所有的 实例化构造 必须以
const
修饰。 - 工厂构造 只能返回已知的一个枚举实例。
- 由于
Enum
已经自动进行了继承,所以枚举类不能再继承其他类。 - 不能重载
index
、hashCode
和比较操作符==
。 - 不能声明
values
字段,否则它将与枚举本身的静态values
getter 冲突。 - 在进行枚举定义时,所有的实例都需要首先进行声明,且至少要声明一个枚举实例。
下方是一个增强枚举的例子,它包含多个枚举实例、成员变量、getter 并且实现了接口:
1 | enum Vehicle implements Comparable<Vehicle> { |
增强枚举仅在 Dart SDK 版本 2.17 以上可用。
使用枚举
你可以像访问 静态变量 一样访问枚举值:
1 | final favoriteColor = Color.blue; |
每一个枚举值都有一个名为 index
成员变量的 Getter 方法,该方法将会返回以 0 为基准索引的位置值。例如,第一个枚举值的索引是 0 ,第二个枚举值的索引是 1。以此类推。
1 | assert(Color.red.index == 0); |
想要获得全部的枚举值,使用枚举类的 values
方法获取包含它们的列表:
1 | List<Color> colors = Color.values; |
你可以在 Switch 语句中使用枚举,但是需要注意的是必须处理枚举值的每一种情况,即每一个枚举值都必须成为一个 case 子句,不然会出现警告:
1 | var aColor = Color.blue; |
如果你想要获取一个枚举值的名称,例如 Color.blue
的 'blue'
,请使用 .name
属性:
1 | print(Color.blue.name); // 'blue' |
使用 Mixin 为类添加功能
Mixin 是一种在多重继承中复用某个类中代码的方法模式。
使用 with
关键字并在其后跟上 Mixin 类的名字来使用 Mixin 模式:
1 | class Musician extends Performer with Musical { |
想要实现一个 Mixin,请创建一个继承自 Object 且未声明构造函数的类。除非你想让该类与普通的类一样可以被正常地使用,否则请使用关键字 mixin
替代 class
。例如:
1 | mixin Musical { |
可以使用关键字 on
来指定哪些类可以使用该 Mixin 类,比如有 Mixin 类 A,但是 A 只能被 B 类使用,则可以这样定义 A:
1 | class Musician { |
mixin
关键字在 Dart 2.1 中才被引用支持。早期版本中的代码通常使用abstract class
代替。你可以查阅 Dart SDK 变更日志 和 2.1 mixin 规范 获取更多有关 Mixin 在 2.1 中的变更信息。
类变量和方法
使用关键字 static
可以声明类变量或类方法。
静态变量
静态变量(即类变量)常用于声明类范围内所属的状态变量和常量:
1 | class Queue { |
静态变量在其首次被使用的时候才被初始化。
本文代码准守 风格推荐指南 中的命名规则,使用
驼峰式大小写
来命名常量。
静态方法
静态方法(即类方法)不能对实例进行操作,因此不能使用 this
。但是他们可以访问静态变量。如下面的例子所示,你可以在一个类上直接调用静态方法:
1 | import 'dart:math'; |
对于一些通用或常用的静态方法,应该将其定义为顶级函数而非静态方法。
可以将静态方法作为编译时常量。例如,你可以将静态方法作为一个参数传递给一个常量构造函数。