Dart 的类型系统

Dart 语法跟 Java 比较类似,类型系统也非常相近,但仍然有一些细节需要注意。

[TOC]

1
var i = 10;

以上是一段合法的 Dart 代码。你会误以为 Dart 是动态语言,实际上并非如此。Dart 是强类型的静态语言,它结合使用了静态类型检查和运行时检查。Dart 的聪明之处在于类型推断,所以上述代码中 i 的类型被推断成 int

一些 Tips

静态类型的大多数规则很容易理解,但仍然有些不明显的规则。

规则一

覆盖方法时,使用稳定的返回类型。(原文:Use sound return types when overriding methods.) 换句话说,子类方法的返回类型必须与父类中方法的返回类型相同或是其子类。

考虑 Animal 类中的 getter 方法:

1
2
3
4
class Animal {
void chase(Animal a) { ... }
Animal get parent => ...
}

HoneyBadger 继承自 Animal,所以可以用 HoneyBadger 作为 getter 方法的返回类型,但是不能使用一个跟 Animal 不相关的类型作为 getter 方法的返回类型。

1
2
3
4
class HoneyBadger extends Animal {
void chase(Animal a) { ... }
HoneyBadger get parent => ...
}

规则二

第二条规则跟前一条规则类似。覆盖方法时,使用稳定的参数类型。(原文:Use sound parameter types when overriding methods)。换句话说,子类方法的返回类型必须与父类中方法的返回类型相同或是其超类。注意,这里是不是”收紧”类型,而是”放宽”类型。

Animal 类的 chase(Animal) 方法为例:

1
2
3
4
class Animal {
void chase(Animal a) { ... }
Animal get parent => ...
}

HoneyBadger 继承自 Animal,它的 chase() 方法可以接受 AnimalHoneyBadger 甚至是 Object 作为参数。

1
2
3
4
class HoneyBadger extends Animal {
void chase(Object a) { ... }
Animal get parent => ...
}

(官网中对这条规则有解释,但似乎含糊不清。或者是我自己没有弄懂)

如果不遵守这条规则,会报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal {
void chase(Animal a) {}

}

class Mouse extends Animal {}

class Cat extends Animal {
// Error: The parameter 'x' of the method 'Cat.chase' has type 'Mouse',
// which does not match the corresponding type, 'Animal',
// in the overridden method, 'Animal.chase'

// Change to a supertype of 'Animal',
// or, for a covariant parameter, a subtype.
@override
void chase(Mouse x) {}
}

规则三

动态列表(dynamic list)可以包含不同种类的内容。但是将动态列表作为一个有类型的列表是错误的。

1
2
3
4
5
6
7
8
9
10
11
class Animal {}

class Cat extends Animal { }

class Dog extends Animal { }

void main() {
List<Cat> foo = <dynamic>[Cat()]; // Error
List<Cat> foo2 = <dynamic>[Dog)()]; // Error
//List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK
}

类型推断

Dart 的类型推断可以让代码更简洁。

1
Map<String, dynamic> arguments = {'argA': 'hello', 'argB': 42};

使用 var 并让 Dart 推断类型:

1
var arguments = {'argA': 'hello', 'argB': 42}; // Map<String, Object>

这里给出几个类型推断的示例:

1
2
3
4
5
6
7
8
9
10
// Inferred as if you wrote <int>[].
List<int> listOfInt = [];

// Inferred as if you wrote <double>[3.0].
var listOfDouble = [3.0];

// Inferred as Iterable<int>
var ints = listOfDouble.map(
// 使用向下信息将x推断为double, 使用向上信息将闭包的返回类型推断为int
(x) => x.toInt());

类型替换

覆盖方法时,可能会使用新类型替换旧类型。比如在上述规则一和规则二中,分别替换方法的返回值类型和参数类型。什么时候可以用子类型或超类型替换当前类型?

生产者消费者 角度进行思考,有助于回答这个问题。

  • 消费者吸收类型,生产者产生类型 (A consumer absorbs a type and a producer generates a type)
  • 可以将消费者的类型替换为超类型,将生产者的类型替换为子类型 (You can replace a consumer’s type with a supertype and a producer’s type with a subtype)

生产者与消费者

上图中 chase() 方法是消费者,所以可以将参数类型替换成超类型。get parent 是生产者,所以可以将返回类型替换成子类型。

在以下这个例子中,Cat c 是消费者,Cat() 是生产者。

1
Cat c = Cat();

所以可以将消费者类型替换为超类型:

1
Animal c = Cat();

所以也可以将生产者类型替换为子类型:

1
2
// MaineCoon 是 Cat 的子类
Cat c = MaineCoon();

泛型

Dart 在运行时保留了泛型的类型信息。Java 与此不同,它在运行时已擦除类型信息。

在 Java 中,你可以判断一个对象是否 List,但无法判断一个对象是否 List<String>。Java 中,即便 CatAnimal 的子类,但 List<Cat> 并不是 List<Animal> 的子类型。所以如下 Java 代码是错误的:

1
2
// 错误: 不兼容的类型: ArrayList<Cat>无法转换为List<Animal>
List<Animal> list = new ArrayList<Cat>();

但在 Dart 中,上述限制不存在。List<Cat>List<Animal> 的子类型。List 之间甚至也可以有下图中这样的子类关系。

List 类型之间的关系

在 Dart 中,如下代码可以正常工作:

1
2
// ok
List<Animal> foo = List<Cat>();

参考资料