Dart 类型系统
Dart 语言是类型安全的:它结合了静态类型检查和运行时检查,以确保变量的值始终与变量的静态类型匹配,有时被称为健全的类型。尽管类型是强制性的,但由于类型推断,类型注释是可选的。
静态类型检查的一个好处是能够使用 Dart 的静态分析器在编译时发现错误。
您可以通过向泛型类添加类型注释来修复大多数静态分析错误。最常见的泛型类是集合类型 List<T>
和 Map<K,V>
。
例如,在以下代码中,printInts()
函数打印一个整数列表,main()
创建一个列表并将其传递给 printInts()
。
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')
中的引号会生成通过静态分析的代码,并且运行没有错误或警告。
void printInts(List<int> a) => print(a);
void main() {
final list = <int>[];
list.add(1);
list.add(2);
printInts(list);
}
什么是健全性?
#健全性是关于确保您的程序不会进入某些无效状态。一个健全的类型系统意味着您永远不会进入表达式的计算结果与表达式的静态类型不匹配的状态。例如,如果一个表达式的静态类型是 String
,那么在运行时,您可以保证在评估它时只会得到一个字符串。
Dart 的类型系统与 Java 和 C# 中的类型系统一样,是健全的。它使用静态检查(编译时错误)和运行时检查的组合来强制执行这种健全性。例如,将 String
分配给 int
是编译时错误。如果对象不是 String
,则使用 as String
将对象转换为 String
将失败并出现运行时错误。
健全性的好处
#健全的类型系统有几个好处
在编译时揭示与类型相关的错误。
一个健全的类型系统强制代码明确其类型,因此在运行时可能难以找到的与类型相关的错误会在编译时被揭示出来。更具可读性的代码。
代码更易于阅读,因为您可以依赖于值实际上具有指定的类型。在健全的 Dart 中,类型不能说谎。更易于维护的代码。
使用健全的类型系统,当您更改一段代码时,类型系统可以警告您刚刚中断的其他代码段。更好的提前 (AOT) 编译。
虽然可以在没有类型的情况下进行 AOT 编译,但生成的代码效率要低得多。
通过静态分析的技巧
#大多数静态类型规则都易于理解。以下是一些不太明显的规则
- 重写方法时使用健全的返回类型。
- 重写方法时使用健全的参数类型。
- 不要使用动态列表作为类型化列表。
让我们详细了解这些规则,并使用以下类型层次结构的示例
重写方法时使用健全的返回类型
#子类中方法的返回类型必须与超类中方法的返回类型相同类型或子类型。考虑 Animal
类中的 getter 方法
class Animal {
void chase(Animal a) { ... }
Animal get parent => ...
}
parent
getter 方法返回一个 Animal
。在 HoneyBadger
子类中,您可以将 getter 的返回类型替换为 HoneyBadger
(或 Animal
的任何其他子类型),但不允许使用不相关的类型。
class HoneyBadger extends Animal {
@override
void chase(Animal a) { ... }
@override
HoneyBadger get parent => ...
}
class HoneyBadger extends Animal {
@override
void chase(Animal a) { ... }
@override
Root get parent => ...
}
重写方法时使用健全的参数类型
#重写方法的参数必须具有与超类中相应参数相同或超类型。不要通过用原始参数的子类型替换类型来“收紧”参数类型。
考虑 Animal
类的 chase(Animal)
方法
class Animal {
void chase(Animal a) { ... }
Animal get parent => ...
}
chase()
方法接受一个 Animal
。HoneyBadger
可以追逐任何东西。可以重写 chase()
方法以接受任何东西 (Object
)。
class HoneyBadger extends Animal {
@override
void chase(Object a) { ... }
@override
Animal get parent => ...
}
以下代码将 chase()
方法上的参数从 Animal
收紧到 Mouse
,这是 Animal
的子类。
class Mouse extends Animal { ... }
class Cat extends Animal {
@override
void chase(Mouse a) { ... }
}
此代码不是类型安全的,因为它可能定义一只猫并让它追逐鳄鱼
Animal a = Cat();
a.chase(Alligator()); // Not type safe or feline safe.
不要使用动态列表作为类型化列表
#当您想在列表中包含不同种类的事物时,dynamic
列表很好。但是,您不能将 dynamic
列表用作类型化列表。
此规则也适用于泛型类型的实例。
以下代码创建了一个 Dog
类型的 dynamic
列表,并将其赋值给 Cat
类型的列表,这会在静态分析期间生成错误。
void main() {
List<Cat> foo = <dynamic>[Dog()]; // Error
List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK
}
运行时检查
#运行时检查处理在编译时无法检测到的类型安全问题。
例如,以下代码在运行时会抛出异常,因为将狗的列表转换为猫的列表是错误的。
void main() {
List<Animal> animals = <Dog>[Dog()];
List<Cat> cats = animals as List<Cat>;
}
类型推断
#分析器可以为字段、方法、局部变量和大多数泛型类型参数推断类型。当分析器没有足够的信息来推断特定类型时,它会使用 dynamic
类型。
以下是一个关于类型推断如何使用泛型的示例。在这个例子中,一个名为 arguments
的变量持有一个将字符串键与各种类型的值配对的映射。
如果显式声明变量类型,你可能会这样写:
Map<String, dynamic> arguments = {'argA': 'hello', 'argB': 42};
或者,你可以使用 var
或 final
,让 Dart 推断类型:
var arguments = {'argA': 'hello', 'argB': 42}; // Map<String, Object>
映射字面量会从其条目推断类型,然后变量会从映射字面量的类型推断类型。在这个映射中,键都是字符串,但值具有不同的类型 (String
和 int
,它们的上限是 Object
)。因此,映射字面量的类型为 Map<String, Object>
,变量 arguments
也是如此。
字段和方法推断
#一个没有指定类型且覆盖了超类中的字段或方法,会继承超类方法或字段的类型。
一个没有声明或继承类型但使用初始值声明的字段,会根据初始值获得推断类型。
静态字段推断
#静态字段和变量从它们的初始化器推断类型。请注意,如果遇到循环(即,为变量推断类型依赖于知道该变量的类型),则推断会失败。
局部变量推断
#局部变量的类型从它们的初始化器(如果有)推断得出。后续的赋值不会被考虑在内。这可能意味着会推断出过于精确的类型。如果是这种情况,你可以添加类型注解。
var x = 3; // x is inferred as an int.
x = 4.0;
num y = 3; // A num can be double or int.
y = 4.0;
类型参数推断
#构造函数调用和 泛型方法 调用的类型参数是根据出现上下文的向下信息,以及构造函数或泛型方法的参数的向上信息组合推断出来的。如果推断没有达到你想要或预期的效果,你始终可以显式指定类型参数。
// 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>
。
替换类型
#当你覆盖一个方法时,你正在用可能具有新类型(在新方法中)的东西替换具有一种类型(在旧方法中)的东西。类似地,当你将一个参数传递给一个函数时,你正在用具有另一种类型(实际参数)的东西替换具有一种类型(具有声明类型的参数)的东西。什么时候可以用具有子类型或超类型的东西替换具有一种类型的东西?
在替换类型时,从消费者和生产者的角度进行思考会很有帮助。消费者吸收一种类型,而生产者生成一种类型。
你可以用超类型替换消费者的类型,用子类型替换生产者的类型。
让我们看看简单类型赋值和泛型类型赋值的示例。
简单类型赋值
#当将对象赋值给对象时,什么时候可以用不同的类型替换类型?答案取决于该对象是消费者还是生产者。
考虑以下类型层次结构:
考虑以下简单赋值,其中 Cat c
是一个消费者,而 Cat()
是一个生产者:
Cat c = Cat();
在消费位置,用可以消费任何类型 (Animal
) 的东西替换可以消费特定类型 (Cat
) 的东西是安全的,因此允许将 Cat c
替换为 Animal c
,因为 Animal
是 Cat
的超类型。
Animal c = Cat();
但是将 Cat c
替换为 MaineCoon c
会破坏类型安全,因为超类可能会提供具有不同行为的 Cat 类型,例如 Lion
。
MaineCoon c = Cat();
在生产位置,用更具体的类型 (MaineCoon
) 替换可以生成类型 (Cat
) 的东西是安全的。因此,允许以下操作:
Cat c = MaineCoon();
泛型类型赋值
#对于泛型类型,规则是否相同?是的。考虑动物列表的层次结构 - Cat
的 List
是 Animal
的 List
的子类型,并且是 MaineCoon
的 List
的超类型:
在以下示例中,你可以将 MaineCoon
列表分配给 myCats
,因为 List<MaineCoon>
是 List<Cat>
的子类型:
List<MaineCoon> myMaineCoons = ...
List<Cat> myCats = myMaineCoons;
反方向呢?可以将 Animal
列表分配给 List<Cat>
吗?
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals;
此赋值不会通过静态分析,因为它会创建一个隐式的向下转型,这是不允许从非 dynamic
类型(如 Animal
)进行转换的。
要使此类代码通过静态分析,你可以使用显式转换。
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals as List<Cat>;
但是,显式转换仍然可能在运行时失败,具体取决于要转换的列表 (myAnimals
) 的实际类型。
方法
#覆盖方法时,生产者和消费者规则仍然适用。例如:
对于消费者(例如 chase(Animal)
方法),你可以将参数类型替换为超类型。对于生产者(例如 parent
getter 方法),你可以将返回类型替换为子类型。
有关更多信息,请参阅覆盖方法时使用可靠的返回类型和覆盖方法时使用可靠的参数类型。
其他资源
#以下资源提供了有关可靠 Dart 的更多信息:
- 修复常见的类型问题 - 你在编写可靠的 Dart 代码时可能遇到的错误,以及如何修复它们。
- 修复类型提升失败 - 了解并学习如何修复类型提升错误。
- 可靠的空安全 - 了解如何使用可靠的空安全编写代码。
- 自定义静态分析 - 如何使用分析选项文件设置和自定义分析器和 linter。
除非另有说明,否则本网站上的文档反映的是 Dart 3.6.0。页面上次更新时间为 2024-12-16。 查看源代码 或 报告问题。