跳到主要内容

Dart 类型系统

Dart 语言是类型安全的:它结合了静态类型检查和运行时检查,以确保变量的值始终与其静态类型匹配,有时称为健全类型。虽然类型是强制性的,但由于类型推断,类型注解是可选的。

静态类型检查的一个好处是能够在编译时使用 Dart 的静态分析器发现错误。

您可以通过向泛型类添加类型注解来修复大多数静态分析错误。最常见的泛型类是集合类型 List<T>Map<K,V>

例如,在以下代码中,printInts() 函数打印一个整数列表,main() 创建一个列表并将其传递给 printInts()

✗ 静态分析:失败dart
void printInts(List<int> a) => print(a);

void main() {
  final list = [];
  list.add(1);
  list.add('2');
  printInts(list);
}

前面的代码在调用 printInts(list) 时,在 list 上产生类型错误(如上突出显示)。

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

此错误突出显示了从 List<dynamic>List<int> 的不健全的隐式转换。list 变量的静态类型为 List<dynamic>。这是因为初始化声明 var list = [] 没有为分析器提供足够的信息来推断比 dynamic 更具体的类型参数。printInts() 函数期望类型为 List<int> 的参数,从而导致类型不匹配。

当在创建列表时添加类型注解(<int>)(如下突出显示)时,分析器会抱怨字符串参数无法分配给 int 参数。删除 list.add('2') 中的引号会导致代码通过静态分析,并且运行没有错误或警告。

✔ 静态分析:成功dart
void printInts(List<int> a) => print(a);

void main() {
  final list = <int>[];
  list.add(1);
  list.add(2);
  printInts(list);
}

在 DartPad 中尝试.

什么是健全性?

#

健全性 是关于确保您的程序不会进入某些无效状态。健全的类型系统 意味着您永远不会进入表达式求值为与其静态类型不匹配的值的状态。例如,如果表达式的静态类型为 String,则在运行时,您可以保证在求值时只会得到一个字符串。

Dart 的类型系统,就像 Java 和 C# 中的类型系统一样,是健全的。它使用静态检查(编译时错误)和运行时检查的组合来强制执行健全性。例如,将 String 赋值给 int 是编译时错误。如果对象不是 String,则使用 as String 将对象强制转换为 String 会因运行时错误而失败。

健全性的好处

#

健全的类型系统有几个好处

  • 在编译时揭示与类型相关的错误。
    健全的类型系统强制代码在其类型方面是明确的,因此可能难以在运行时找到的与类型相关的错误会在编译时被揭示。

  • 更易读的代码。
    代码更容易阅读,因为您可以依赖值实际具有指定的类型。在健全的 Dart 中,类型不会说谎。

  • 更易维护的代码。
    使用健全的类型系统,当您更改一段代码时,类型系统可以警告您刚刚破坏的其他代码。

  • 更好的提前 (AOT) 编译。
    虽然没有类型也可以进行 AOT 编译,但生成的代码效率要低得多。

通过静态分析的技巧

#

大多数静态类型的规则都很容易理解。以下是一些不太明显的规则

  • 重写方法时使用健全的返回类型。
  • 重写方法时使用健全的参数类型。
  • 不要将动态列表用作类型化列表。

让我们详细了解这些规则,并使用以下类型层次结构示例

a hierarchy of animals where the supertype is Animal and the subtypes are Alligator, Cat, and HoneyBadger. Cat has the subtypes of Lion and MaineCoon

重写方法时使用健全的返回类型

#

子类中方法的返回类型必须与超类中方法的返回类型相同或为其子类型。考虑 Animal 类中的 getter 方法

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

parent getter 方法返回一个 Animal。在 HoneyBadger 子类中,您可以将 getter 的返回类型替换为 HoneyBadger(或 Animal 的任何其他子类型),但不允许使用不相关的类型。

✔ 静态分析:成功dart
class HoneyBadger extends Animal {
  @override
  void chase(Animal a) {
     ...
  }

  @override
  HoneyBadger get parent => ...
}
✗ 静态分析:失败dart
class HoneyBadger extends Animal {
  @override
  void chase(Animal a) {
     ...
  }

  @override
  Root get parent => ...
}

重写方法时使用健全的参数类型

#

重写方法的参数必须具有与超类中相应参数相同的类型或超类型。不要通过将类型替换为原始参数的子类型来“收紧”参数类型。

考虑 Animal 类的 chase(Animal) 方法

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

chase() 方法接受一个 Animal。蜜獾追逐任何东西。可以重写 chase() 方法以接受任何东西 (Object)。

✔ 静态分析:成功dart
class HoneyBadger extends Animal {
  @override
  void chase(Object a) {
     ...
  }

  @override
  Animal get parent => ...
}

以下代码将 chase() 方法上的参数从 Animal 收紧为 MouseMouseAnimal 的子类。

✗ 静态分析:失败dart
class Mouse extends Animal {
   ...
}

class Cat extends Animal {
  @override
  void chase(Mouse a) {
     ...
  }
}

此代码不是类型安全的,因为这样就可以定义一只猫并将其派去追逐鳄鱼

dart
Animal a = Cat();
a.chase(Alligator()); // Not type safe or feline safe.

不要将动态列表用作类型化列表

#

当您想要拥有一个包含不同类型事物的列表时,动态列表很好。但是,您不能将动态列表用作类型化列表。

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

以下代码创建一个 Dog 的动态列表,并将其分配给类型为 Cat 的列表,这会在静态分析期间生成错误。

✗ 静态分析:失败dart
void main() {
  List<Cat> foo = <dynamic>[Dog()]; // Error
  List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK
}

运行时检查

#

运行时检查处理在编译时无法检测到的类型安全问题。

例如,以下代码在运行时抛出异常,因为将狗列表强制转换为猫列表是错误的

✗ 运行时:失败dart
void main() {
  List<Animal> animals = <Dog>[Dog()];
  List<Cat> cats = animals as List<Cat>;
}

dynamic 的隐式向下转型

#

静态类型为 dynamic 的表达式可以隐式转换为更具体的类型。如果实际类型不匹配,则强制转换会在运行时抛出错误。考虑以下 assumeString 方法

✔ 静态分析:成功dart
int assumeString(dynamic object) {
  String string = object; // Check at run time that `object` is a `String`.
  return string.length;
}

在此示例中,如果 objectString,则强制转换成功。如果它不是 String 的子类型,例如 int,则会抛出 TypeError

✗ 运行时:失败dart
final length = assumeString(1);

类型推断

#

分析器可以推断字段、方法、局部变量和大多数泛型类型参数的类型。当分析器没有足够的信息来推断特定类型时,它会使用 dynamic 类型。

这是一个类型推断如何与泛型一起工作的示例。在此示例中,名为 arguments 的变量保存一个映射,该映射将字符串键与各种类型的值配对。

如果您显式键入变量,则可以这样写

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

或者,您可以使用 varfinal 并让 Dart 推断类型

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

映射字面量从其条目推断其类型,然后变量从映射字面量的类型推断其类型。在此映射中,键都是字符串,但值具有不同的类型(Stringint,它们的上限为 Object)。因此,映射字面量的类型为 Map<String, Object>arguments 变量也是如此。

字段和方法推断

#

没有指定类型并且覆盖超类中的字段或方法的字段或方法,会继承超类方法或字段的类型。

没有声明或继承类型但使用初始值声明的字段,会根据初始值获得推断类型。

静态字段推断

#

静态字段和变量从它们的初始化器推断类型。请注意,如果推断遇到循环(即,为变量推断类型取决于知道该变量的类型),则推断会失败。

局部变量推断

#

局部变量类型从它们的初始化器(如果有)推断。后续赋值不考虑在内。这可能意味着可能会推断出过于精确的类型。如果是这样,您可以添加类型注解。

✗ 静态分析:失败dart
var x = 3; // x is inferred as an int.
x = 4.0;
✔ 静态分析:成功dart
num y = 3; // A num can be double or int.
y = 4.0;

类型参数推断

#

构造函数调用和泛型方法调用的类型参数是根据来自出现上下文的向下信息和来自构造函数或泛型方法参数的向上信息的组合推断出来的。如果推断没有达到您想要或期望的效果,您可以始终显式指定类型参数。

✔ 静态分析:成功dart
// 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());

在最后一个示例中,x 使用向下信息推断为 double。闭包的返回类型使用向上信息推断为 int。Dart 在推断 map() 方法的类型参数 <int> 时,使用此返回类型作为向上信息。

使用边界的推断

#

使用使用边界的推断功能,Dart 的类型推断算法通过将现有约束与声明的类型边界相结合来生成约束,而不仅仅是尽力而为的近似值。

这对于 F 绑定 类型尤其重要,其中使用边界的推断正确地推断出,在下面的示例中,X 可以绑定到 B。如果没有此功能,则必须显式指定类型参数:f<B>(C())

dart
class A<X extends A<X>> {}

class B extends A<B> {}

class C extends B {}

void f<X extends A<X>>(X x) {}

void main() {
  f(B()); // OK.

  // OK. Without using bounds, inference relying on best-effort approximations
  // would fail after detecting that `C` is not a subtype of `A<C>`.
  f(C());

  f<B>(C()); // OK.
}

这是一个使用 Dart 中常见的类型(如 intnum)的更实际的示例

dart
X max<X extends Comparable<X>>(X x1, X x2) => x1.compareTo(x2) > 0 ? x1 : x2;

void main() {
  // Inferred as `max<num>(3, 7)` with the feature, fails without it.
  max(3, 7);
}

使用使用边界的推断,Dart 可以解构类型参数,从泛型类型参数的边界中提取类型信息。这允许以下示例中的 f 等函数同时保留特定的可迭代类型(ListSet)和元素类型。在使用边界的推断之前,如果不丢失类型安全或特定类型信息,这是不可能的。

dart
(X, Y) f<X extends Iterable<Y>, Y>(X x) => (x, x.first);

void main() {
  var (myList, myInt) = f([1]);
  myInt.whatever; // Compile-time error, `myInt` has type `int`.

  var (mySet, myString) = f({'Hello!'});
  mySet.union({}); // Works, `mySet` has type `Set<String>`.
}

如果不使用边界的推断,myInt 将具有 dynamic 类型。以前的推断算法不会在编译时捕获不正确的表达式 myInt.whatever,而会在运行时抛出错误。相反,mySet.union({}) 在不使用边界的推断的情况下将是编译时错误,因为以前的算法无法保留 mySetSet 的信息。

有关使用边界的推断算法的更多信息,请阅读设计文档

类型替换

#

当您重写一个方法时,您正在用可能具有新类型(在新方法中)的东西替换一种类型的东西(在旧方法中)。同样,当您将参数传递给函数时,您正在用具有另一种类型(实际参数)的东西替换具有一种类型(具有声明类型的参数)的东西。什么时候可以用具有子类型或超类型的东西替换具有一种类型的东西?

当替换类型时,从消费者和生产者的角度思考会有所帮助。消费者吸收一种类型,生产者生成一种类型。

您可以将消费者的类型替换为超类型,并将生产者的类型替换为子类型。

让我们看看简单类型赋值和泛型类型赋值的示例。

简单类型赋值

#

当将对象赋值给对象时,什么时候可以用不同的类型替换类型?答案取决于对象是消费者还是生产者。

考虑以下类型层次结构

a hierarchy of animals where the supertype is Animal and the subtypes are Alligator, Cat, and HoneyBadger. Cat has the subtypes of Lion and MaineCoon

考虑以下简单赋值,其中 Cat c 是消费者,Cat() 是生产者

dart
Cat c = Cat();

在消费位置,将消耗特定类型 (Cat) 的东西替换为消耗任何东西 (Animal) 的东西是安全的,因此允许将 Cat c 替换为 Animal c,因为 AnimalCat 的超类型。

✔ 静态分析:成功dart
Animal c = Cat();

但是将 Cat c 替换为 MaineCoon c 会破坏类型安全,因为超类可能会提供具有不同行为的 Cat 类型,例如 Lion

✗ 静态分析:失败dart
MaineCoon c = Cat();

在生产位置,将生成类型 (Cat) 的东西替换为更具体的类型 (MaineCoon) 是安全的。因此,允许以下操作

✔ 静态分析:成功dart
Cat c = MaineCoon();

泛型类型赋值

#

泛型类型的规则是否相同?是的。考虑动物列表的层次结构——List<Cat>List<Animal> 的子类型,是 List<MaineCoon> 的超类型

List<Animal> -> List<Cat> -> List<MaineCoon>

在以下示例中,您可以将 MaineCoon 列表分配给 myCats,因为 List<MaineCoon>List<Cat> 的子类型

✔ 静态分析:成功dart
List<MaineCoon> myMaineCoons = ...
List<Cat> myCats = myMaineCoons;

反过来呢?可以将 Animal 列表分配给 List<Cat> 吗?

✗ 静态分析:失败dart
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals;

此赋值未通过静态分析,因为它创建了一个隐式向下转型,这是非 dynamic 类型(例如 Animal)所不允许的。

要使此类代码通过静态分析,您可以使用显式强制转换。

dart
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals as List<Cat>;

但是,显式强制转换仍然可能在运行时失败,具体取决于被强制转换的列表(myAnimals)的实际类型。

方法

#

当重写方法时,生产者和消费者的规则仍然适用。例如

Animal class showing the chase method as the consumer and the parent getter as the producer

对于消费者(例如 chase(Animal) 方法),您可以将参数类型替换为超类型。对于生产者(例如 parent getter 方法),您可以将返回类型替换为子类型。

有关更多信息,请参阅重写方法时使用健全的返回类型重写方法时使用健全的参数类型

协变参数

#

一些(很少使用)编码模式依赖于通过用子类型覆盖参数类型来收紧类型,这是无效的。在这种情况下,您可以使用 covariant 关键字告诉分析器您是有意这样做。这会消除静态错误,而是在运行时检查无效的参数类型。

以下展示了您可能如何使用 covariant

✔ 静态分析:成功dart
class Animal {
  void chase(Animal x) {
     ...
  }
}

class Mouse extends Animal {
   ...
}

class Cat extends Animal {
  @override
  void chase(covariant Mouse x) {
     ...
  }
}

虽然此示例显示在子类型中使用 covariant,但 covariant 关键字可以放在超类或子类方法中。通常,超类方法是放置它的最佳位置。covariant 关键字适用于单个参数,也支持 setter 和字段。

其他资源

#

以下资源包含有关健全 Dart 的更多信息