Dart 是支持基于 mixin 继承机制的面向对象语言,所有对象都是一个类的实例,而所有的类都继承自 Object 类。基于 mixin 的继承 意味着每个除 Object 类之外的类都只有一个超类,一个类的代码可以在其它多个类继承中重复使用。 Extension 方法 是一种在不更改类或创建子类的情况下向类添加功能的方式。
使用 ?. 代替 . 可以避免因为左边表达式为 null 而导致的问题:
// 如果 p 为非空则将其属性 y 的值设为 4
p?.y = 4;
构造函数
以使用 构造函数 来创建一个对象。构造函数的命名方式可以为 类名 或 类名.
标识符 的形式。例如下述代码分别使用 Point()
和 Point.fromJson()
两种构造器创建了 Point 对象:
var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});
//以下代码具有相同的效果,但是构造函数名前面的的 new 关键字是可选的:
var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});
常量构造函数
在构造函数名之前加 const 关键字,来创建编译时常量时
var p = const ImmutablePoint(2, 2);
//两个使用相同构造函数相同参数值构造的编译时常量是同一个对象:
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);
assert(identical(a, b)); // 它们是同一个实例 (They are the same instance!)
获取对象的类型
可以使用 Object 对象的 runtimeType
属性在运行时获取一个对象的类型,该对象类型是 Type 的实例。
print('The type of a is ${a.runtimeType}');
实例变量
下面是声明实例变量的示例:
class Point {
double x; // 声明 double 变量 x 并初始化为 null。
double y; // 声明 double 变量 y 并初始化为 null
double z = 0; // 声明 double 变量 z 并初始化为 0。
}
所有未初始化的实例变量其值均为 null。
所有实例变量均会隐式地声明一个 Getter 方法,非 final 类型的实例变量还会隐式地声明一个 Setter 方法。
class Point {
double x;
double y;
}
void main() {
var point = Point();
point.x = 4; // 使用 x 的 Setter 方法。
assert(point.x == 4); // 使用 x 的 Getter 方法。
assert(point.y == null); // 默认值为 null。
}
构造函数
声明一个与类名一样的函数即可声明一个构造函数
class Point {
double x, y;
Point(double x, double y) {
// 还会有更好的方式来实现此逻辑,敬请期待。
this.x = x;
this.y = y;
}
// Dart 则提供了一种特殊的语法糖来简化该步骤:
class Point {
double x, y;
// 在构造函数体执行前用于设置 x 和 y 的语法糖。
Point(this.x, this.y);
}
}
默认构造函数
如果你没有声明构造函数,那么 Dart 会自动生成一个无参数的构造函数并且该构造函数会调用其父类的无参数构造方法。
构造函数不被继承
子类不会继承父类的构造函数,如果子类没有声明构造函数,那么只会有一个默认无参数的构造函数。
命名式构造函数
可以为一个类声明多个命名式构造函数来表达更明确的意图
class Point {
double x, y;
Point(this.x, this.y);
// 命名式构造函数
Point.origin() {
x = 0;
y = 0;
}
}
记住构造函数是不能被继承的,这将意味着子类不能继承父类的命名式构造函数,如果你想在子类中提供一个与父类命名构造函数名字一样的命名构造函数,则需要在子类中显式地声明。
初始化列表
除了调用父类构造函数之外,还可以在构造函数体执行之前初始化实例变量。每个实例变量之间使用逗号分隔。
// 使用初始化列表在构造函数体执行前设置实例变量。
Point.fromJson(Map<String, double> json)
: x = json['x'],
y = json['y'] {
print('In Point.fromJson(): ($x, $y)');
}
重定向构造函数
有时候类中的构造函数会调用类中其它的构造函数,该重定向构造函数没有函数体,只需在函数签名后使用(:)指定需要重定向到的其它构造函数即可:
class Point {
double x, y;
// 该类的主构造函数。
Point(this.x, this.y);
// 委托实现给主构造函数。
Point.alongXAxis(double x) : this(x, 0);
}
常量构造函数
如果类生成的对象都是不会变的,那么可以在生成这些对象时就将其变为编译时常量。你可以在类的构造函数前加上 const 关键字并确保所有实例变量均为 final 来实现该功能。
class ImmutablePoint {
static final ImmutablePoint origin =
const ImmutablePoint(0, 0);
final double x, y;
const ImmutablePoint(this.x, this.y);
}
工厂构造函数
使用 factory 关键字标识类的构造函数将会令该构造函数变为工厂构造函数,这将意味着使用该构造函数构造类的实例时并非总是会返回新的实例对象。例如,工厂构造函数可能会从缓存中返回一个实例,或者返回一个子类型的实例。
以下示例演示了从缓存中返回对象的工厂构造函数:
class Logger {
final String name;
bool mute = false;
// _cache 变量是库私有的,因为在其名字前面有下划线。
static final Map<String, Logger> _cache =
<String, Logger>{};
factory Logger(String name) {
return _cache.putIfAbsent(
name, () => Logger._internal(name));
}
Logger._internal(this.name);
void log(String msg) {
if (!mute) print(msg);
}
}
在工厂构造函数中无法访问 this。
方法
方法是对象提供行为的函数。
对象的实例方法可以访问实例变量和 this
import 'dart:math';
class Point {
double x, y;
Point(this.x, this.y);
double distanceTo(Point other) {
var dx = x - other.x;
var dy = y - other.y;
return sqrt(dx * dx + dy * dy);
}
}
Getter 和 Setter
Getter 和 Setter 是一对用来读写对象属性的特殊方法,上面说过实例对象的每一个属性都有一个隐式的 Getter 方法,如果为非 final 属性的话还会有一个 Setter 方法,你可以使用 get 和 set 关键字为额外的属性添加 Getter 和 Setter 方法:
class Rectangle {
double left, top, width, height;
Rectangle(this.left, this.top, this.width, this.height);
// 定义两个计算产生的属性:right 和 bottom。
double get right => left + width;
set right(double value) => left = value - width;
double get bottom => top + height;
set bottom(double value) => top = value - height;
}
void main() {
var rect = Rectangle(3, 4, 20, 15);
assert(rect.left == 3);
rect.right = 12;
assert(rect.left == -8);
}
使用 Getter 和 Setter 的好处是,你可以先使用你的实例变量,过一段时间过再将它们包裹成方法且不需要改动任何代码,即先定义后更改且不影响原有逻辑。
抽象方法
实例方法、Getter 方法以及 Setter 方法都可以是抽象的,定义一个接口方法而不去做具体的实现让实现它的类去实现该方法,抽象方法只能存在于 抽象类中。
直接使用分号(;)替代方法体即可声明一个抽象方法:
abstract class Doer {
// 定义实例变量和方法等等……
void doSomething(); // 定义一个抽象方法。
}
class EffectiveDoer extends Doer {
void doSomething() {
// 提供一个实现,所以在这里该方法不再是抽象的……
}
}
抽象类
使用关键字 abstract
标识类可以让该类成为 抽象类,抽象类将无法被实例化。抽象类常用于声明接口方法、有时也会有具体的方法实现。如果想让抽象类同时可被实例化,可以为其定义 工厂构造函数。
抽象类常常会包含 抽象方法。
// 该类被声明为抽象的,因此它不能被实例化。
abstract class AbstractContainer {
// 定义构造函数、字段、方法等……
void updateChildren(); // 抽象方法。
}
隐式接口
每一个类都隐式地定义了一个接口并实现了该接口,这个接口包含所有这个类的实例成员以及这个类所实现的其它接口。如果想要创建一个 A 类支持调用 B 类的 API 且不想继承 B 类,则可以实现 B 类的接口。
一个类可以通过关键字 implements 来实现一个或多个接口并实现每个接口定义的 API:
// Person 类的隐式接口中包含 greet() 方法。
class Person {
// _name 变量同样包含在接口中,但它只是库内可见的。
final _name;
// 构造函数不在接口中。
Person(this._name);
// greet() 方法在接口中。
String greet(String who) => '你好,$who。我是$_name。';
}
// Person 接口的一个实现。
class Impostor implements Person {
get _name => '';
String greet(String who) => '你好$who。你知道我是谁吗?';
}
String greetBob(Person person) => person.greet('小芳');
void main() {
print(greetBob(Person('小芸')));
print(greetBob(Impostor()));
}
如果需要实现多个类接口,可以使用逗号分割每个接口类:
class Point implements Comparable, Location {...}
扩展一个类
使用 extends 关键字来创建一个子类,并可使用 super 关键字引用一个父类:
class Television {
void turnOn() {
_illuminateDisplay();
_activateIrSensor();
}
// ···
}
class SmartTelevision extends Television {
void turnOn() {
super.turnOn();
_bootNetworkInterface();
_initializeMemory();
_upgradeApps();
}
// ···
}
重写类成员
子类可以重写父类的实例方法、Getter 以及 Setter 方法。你可以使用 @override 注解来表示你重写了一个成员:
class SmartTelevision extends Television {
@override
void turnOn() {...}
// ···
}
重写运算符
可以在一个类中重写下表所罗列出的所有运算符。比如如果定一个 Vector 表示矢量的类,那么可以考虑重写 + 操作符来处理两个矢量的相加。
运算符 | 运算符 | 运算符 | 运算符 |
---|---|---|---|
< | + | | | [] |
> | / | ^ | []= |
<= | ~/ | & | ~ |
>= | * | << | == |
– | % | >> |
必须要注意的是 != 操作符并不是一个可被重写的操作符。表达式 e1 != e2 仅仅是 !(e1 == e2) 的一个语法糖。
下面是重写 + 和 - 操作符的例子:
class Vector {
final int x, y;
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);
// 运算符 == 和 hashCode 的实现未在这里展示,详情请查看下方说明。
// ···
}
void main() {
final v = Vector(2, 3);
final w = Vector(2, 2);
assert(v + w == Vector(4, 5));
assert(v - w == Vector(0, 1));
}
枚举类型
枚举类型是一种特殊的类型,也称为 enumerations 或 enums,用于定义一些固定数量的常量值。
使用关键字 enum 来定义枚举类型:
enum Color { red, green, blue }
每一个枚举值都有一个名为 index 成员变量的 Getter 方法,该方法将会返回以 0 为基准索引的位置值。
例如,第一个枚举值的索引是 0 ,第二个枚举值的索引是 1。以此类推。
assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);
可以使用枚举类的 values 方法获取一个包含所有枚举值的列表:
List<Color> colors = Color.values;
assert(colors[2] == Color.blue);
枚举类型有如下两个限制:
- 枚举不能成为子类,也不可以 mix in,你也不可以实现一个枚举。
- 不能显式地实例化一个枚举类。
Mixin 为类添加功能
Mixin 是一种在多重继承中复用某个类中代码的方法模式。使用 with 关键字并在其后跟上 Mixin 类的名字来使用 Mixin 模式:
class Musician extends Performer with Musical {
// ···
}
class Maestro extends Person
with Musical, Aggressive, Demented {
Maestro(String maestroName) {
name = maestroName;
canConduct = true;
}
}
泛型
为什么使用泛型?
泛型常用于需要要求类型安全的情况,但是它也会对代码运行有好处:
- 适当地指定泛型可以更好地帮助代码生成。
- 使用泛型可以减少代码重复。
限制参数化类型
有时使用泛型的时候可能会想限制泛型的类型范围,这时候可以使用 extends
关键字:
class Foo<T extends SomeBaseClass> {
// 具体实现……
String toString() => "'Foo<$T>' 的实例";
}
class Extender extends SomeBaseClass {...}
使用泛型方法
T first<T>(List<T> ts) {
// 处理一些初始化工作或错误检测……
T tmp = ts[0];
// 处理一些额外的检查……
return tmp;
}
方法 first<T> 的泛型 T 可以在如下地方使用:
- 函数的返回值类型 (T)。
- 参数的类型 (List<T>)。
- 局部变量的类型 (T tmp)。
网友评论