Dart 类型系统

Google Flutter 18 年底发布,现在稳定版更新到了 1.5, 已经有了比较完善的开发工具, 三方库以及不错的声明式 UI 开发体验。Google 从 React 上借鉴了很多,同时又使用了强类型的语言 Dart。这篇文章主要会描述 Dart 的类型系统,尤其关注 Dart2 添加的类型安全特性,翻译自官方文档 -- The Dart type system

Dart 语言是类型安全的,它使用了静态类型检查和运行时检查的组合来确保一个变量的值总是匹配它的类型。虽然类型是强制性的,但类型注解是可选的因为有类型推断的存在。

一个静态类型检查的好处是能够在编译时通过 Dart 的静态分析器来发现 bug 。

你可以通过添加类型注解给泛型类的方式修复大部分类型检查错误。最通用的泛型类就是集合类比如 List<T>Map<K, V>

举个例子,下面的代码中 printInts() 函数打印了一个整型数字列表,然后 main() 建立了一个列表然后作为参数传递给了 printInts()

这种调用方式会导致一个类型错误:

error • The argument type 'List' can't be assigned to the parameter type 'List<int>' • argument_type_not_assignable

这个错误提示发生了一个不健全的隐式转换从 List<dynamic>List<int> 。这个 list 变量在作为参数时已经有了静态类型 List<dynamic>,这是因为在初始化声明时我们没有提供任何比 dynamic 更为具体的类型信息来给分析器。而 printInts() 函数要求一个类型为 List<int> 的参数,所以导致了类型不匹配。

当在初始化添加一个类型注解 <int> 后,分析器给我们提示一个字符串参数不能被赋值给一个整型参数的错误。把 list.add("2") 修改为 list.add(2) 后则能够通过静态分析,并且运行也不会有警告或者错误。

什么是 soundness

Soundness (后面称为可靠性)是确保你的程序不能进入某些无效的状态。一个可靠的类型系统意味着你永远不会进入一个表达式计算的值不匹配表达式的静态类型的状态。举个例子,如果一个表达式的静态类型是 String, 在运行时你可以确保它就是一个字符串。

Dart 的类型系统,类似 Java 以及 C#, 是可靠的。它通过静态类型检查和运行时检查的组合来强制保证可靠性。举例来讲,把一个 String 赋值给 int 变量是一个编译时错误,转换一个 Object 去当做一个 String 会报一个运行时错误如果这个对象不是一个字符串。

soundness 的好处

一个可靠的类型系统会有很多好处:

-- 在编译时期发现类型相关的错误
-- 代码更具备可读性
-- 更易维护的代码
-- 更好的提前编译(AOT)

通过静态分析的 Tips

大部分的静态类型规则是很好理解的,这里也有一些不太明显的规则:

-- 重载方法时声明可靠的返回类型
-- 重载方法时声明可靠的参数类型
-- 不要使用动态类型列表作为一个有类型的列表

让我们更细节的了解这些规则,通过一个下面标注类型层次的示例:

hierarchy

重载时使用可靠的返回值类型

子类方法的返回值类型必须要是父类声明的相同类型或者其子类,下面是一个 Animal 类的 getter 方法:

class Animal {
  void chase(Animal a) { ... }
  Animal get parent => ...
}

父类返回了 Animal ,而在一个 HoneyBadger 的子类,你可以返回这个 Animal 的子类比如 HoneyBadger, 但是不相关的类型是不允许的:

class HoneyBadger extends Animal {
  void chase(Animal a) { ... }
  HoneyBadger get parent => ... // passing static analysis
}

class HoneyBadger extends Animal {
  void chase(Animal a) { ... }
  Root get parent => ... // error
}

重载时使用可靠的参数类型

重载方法时的参数类型必须是父类表明的相同类型或者是其父类,不要替换为原有参数类型的子类来收紧参数类型。

提示:如果你真的需要使用子类,你可以使用 关键字 covariant

代码示例与上一段类似,这里省略。

不要使用动态类型列表作为一个有类型的列表

当你想使用一个列表包含不同类型对象时一个动态列表是不错的,但是,你不能把它当做一个固定类型的列表。

这条规则也适用于泛型类的实例。

下面的代码创建了一个 Dog 的动态列表,然后给他赋值给 Cat 类型的列表,这将在静态分析时报错。

class Cat extends Animal { ... }

class Dog extends Animal { ... }

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

运行时检查

Dart VMdartdevc 工具中的运行时检查用来处理哪些静态分析器没办法捕获的类型安全问题。

举个例子,下面的代码在运行时抛出了一个异常,因为它错误的把一个 Dog 的列表赋值给了一个 Cat 类的列表:

void main() {
  List<Animal> animals = [Dog()];
  List<Cat> cats = animals;
}

类型推断

静态分析器可以推导出一些成员,方法,泛型参数的类型,当分析器没有足够的信息来推断出一个类型时,它会使用 dynamic 类型。

下面是一个例子关于类型推断泛型时是如何工作的,在这个例子中,一个变量 arguments 引用了一个拥有字符串 key 和不同类型的 value 的 map 。

如果你显式的给变量标注类型,你可能会这么写:

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

或者,你使用一个简单的 var 来让 Dart 推断类型:

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

这个 map 字面量通过它内部条目的类型推断出自己的类型,然后这个 arguments 变量则通过 map 字面量的类型推断出它自己的。在这个 map 中,key 都是字符串,但是值却有不同的类型。所以最终 map 和变量的类型为 Map<String, Object

实例成员

一个变量或者一个方法如果没标注类型,而且他们是从父类重载来的话,则类型继承自父类。

一个成员如果没声明或者也没从父类继承类型但是却有初始值,则会根据他的初始值来推断出它的类型。

类成员

类成员和变量从他们的初始化器来推断类型。注意推断如果遇到循环则会失败。

本地变量

本地变量类型通过它们的初始化器推断,如果有的话。后续的赋值操作则不做考虑。这意味着精确的类型推断。

var x = 3; // x is inferred as an int
x = 4.0;  // error

num y = 3; // a num can be double or int
y = 4.0;   // success

类型参数

带类型参数的额构造函数调用以及泛型方法调用的类型推断是基于组合上下文信息的基础上进行的。如果推理不是你的预期,那可以显式的指定类型。

// 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) => x.toInt());

替换类型

当你重载一个方法时,你可能会替换掉旧方法的一个然后出现一个新的类型。同样的,你在传递一个参数时,也会有类似的情况发生。那么什么时候可以替换掉这些子类或者父类的类型呢?

当替换类型时,把它们思考为 消费者生产者 会有益于理解。一个 消费者 吸收一个类型,一个 生产者 产生一个类型。

你可以换一个 消费者 的父类或者一个 生产者 的子类。

让我们来看一个简单类型赋值和泛型类赋值的例子。

简单类型赋值

当把对象赋值给其他对象时,什么时候你可以替换掉类型呢?答案取决于这个对象是一个*消费者* 还是一个 生产者

先看下面的类型继承关系:

hierarchy

下面简单赋值操作中 Cat c 是一个*消费者* 而 Cat() 则是一个 生产者

Cat c = Cat();

从消费的角度看,它是安全的可以替换掉指定的类型变为 Animal c ,因为 Animal 是 Cat 的父类。

Animal c = Cat();

从生产者角度,你可以把 Cat() 构造中的类型换为 Cat 的子类。

泛型类型赋值

上面的规则对于泛型类型也适用?答案是肯定的。下面这个例子,你可以把一个 MaineCoon 列表赋值给 myCats 因为它是 Cat 的子类:

List<Cat> myCats = List<MaineCoon>();

也可以指定 Cat 的父类 Animal:

List<Cat> myCats = List<Animal>();

这个赋值操作通过了静态分析,不过它产生了一个隐式转换,它等价于:

List<Cat> myCats = List<Animal>() as List<Cat>;

这代码可能会在运行时失败,你可以禁用隐式转换,在 analysis options file 中配置 implicit-casts: false

方法

当重载一个方法时,生产者消费者规则同样有效。比如:

method

如果还想了解更多的信息,可以跳到:Use sound return types when overriding methodsUse sound parameter types when overriding methods

以上就是这篇文章的全部内容,如果还想了解其他资源可以前往:

1) Fixing common type problems
2) Dart 2
3) Customizing static analysis

个人联系方式详见关于

Comments
Write a Comment