深色模式
Dart 类型
内置类型
Dart 语言对以下内容提供了特殊支持:
- 数字(
int
、double
) - 字符串(
String
) - 布尔值(
bool
) - 记录(
(value1, value2)
) - 函数(
Function
) - 列表(
List
,也称为数组) - 集合(
Set
) - 映射(
Map
) - 符文(
Runes
;通常被characters
API 取代) - 符号(
Symbol
) - 空值(
null
,类型为Null
)
这种支持包括使用字面量创建对象的能力。例如,'this is a string'
是一个字符串字面量,true
是一个布尔值字面量。
由于 Dart 中的每个变量都引用一个对象(类的实例),因此通常可以使用构造函数来初始化变量。一些内置类型有它们自己的构造函数。例如,可以使用 Map()
构造函数来创建一个映射。
其他一些类型在 Dart 语言中也有特殊作用:
Object
:除Null
之外所有 Dart 类的超类。Enum
:所有枚举的超类。Future
和Stream
:用于异步支持。Iterable
:用于for - in
循环和同步生成器函数。Never
:表示一个表达式永远无法成功完成求值。通常用于总是抛出异常的函数。dynamic
:表示你想禁用静态检查。通常应该使用Object
或Object?
代替。void
:表示一个值永远不会被使用。通常用作返回类型。
Object
、Object?
、Null
和 Never
类在类层次结构中有特殊作用。在“理解空安全”中了解这些作用。
数字
Dart 数字有两种类型:
int
:整数,其值根据平台不同,最大不超过 64 位。在原生平台上,值的范围可以从 -2⁶³ 到 2⁶³ - 1。在 Web 上,整数值用 JavaScript 数字表示(无小数部分的 64 位浮点值),范围从 -2⁵³ 到 2⁵³ - 1。double
:64 位(双精度)浮点数,遵循 IEEE 754 标准。
int
和 double
都是 num
的子类型。num
类型包含基本运算符,如 +
、-
、/
和 *
,也能找到 abs()
、ceil()
和 floor()
等方法。(位运算符,如 >>
,在 int
类中定义)。如果 num
及其子类型没有你需要的功能,dart:math
库可能会有。
整数是没有小数点的数字。以下是定义整数字面量的一些示例:
dart
var x = 1;
var hex = 0xDEADBEEF;
如果一个数字包含小数点,它就是双精度浮点数。以下是定义双精度浮点数字面量的一些示例:
dart
var y = 1.1;
var exponents = 1.42e5;
你也可以将变量声明为 num
类型。这样做的话,该变量可以是整数也可以是双精度浮点数。
dart
num x = 1; // x 可以是 int 或 double 类型的值
x += 2.5;
必要时,整数字面量会自动转换为双精度浮点数:
dart
double z = 1; // 等同于 double z = 1.0。
以下是将字符串转换为数字,或反之的方法:
dart
// 字符串 -> 整数
var one = int.parse('1');
assert(one == 1);
// 字符串 -> 双精度浮点数
var onePointOne = double.parse('1.1');
assert(onePointOne == 1.1);
// 整数 -> 字符串
String oneAsString = 1.toString();
assert(oneAsString == '1');
// 双精度浮点数 -> 字符串
String piAsString = 3.14159.toStringAsFixed(2);
assert(piAsString == '3.14');
int
类型定义了传统的按位移位(<<
、>>
、>>>
)、取反(~
)、与(&
)、或(|
)和异或(^
)运算符,这些运算符对于操作和屏蔽位域中的标志很有用。例如:
dart
assert((3 << 1) == 6); // 0011 << 1 == 0110
assert((3 | 4) == 7); // 0011 | 0100 == 0111
assert((3 & 4) == 0); // 0011 & 0100 == 0000
更多示例,请参阅按位和移位运算符部分。
数字字面量是编译时常量。许多算术表达式也是编译时常量,只要它们的操作数是求值为数字的编译时常量。
dart
const msPerSecond = 1000;
const secondsUntilRetry = 5;
const msUntilRetry = secondsUntilRetry * msPerSecond;
更多信息,请参阅“Dart 中的数字”。
你可以使用一个或多个下划线(_
)作为数字分隔符,使长数字字面量更易读。多个数字分隔符允许进行更高级别的分组。
dart
var n1 = 1_000_000;
var n2 = 0.000_000_000_01;
var n3 = 0x00_14_22_01_23_45; // MAC 地址
var n4 = 555_123_4567; // 美国电话号码
var n5 = 100__000_000__000_000; // 一百亿亿!
版本说明: 使用数字分隔符需要至少 3.6 的语言版本。
字符串
Dart 字符串(String
对象)保存着 UTF - 16 代码单元序列。你可以使用单引号或双引号来创建字符串:
dart
var s1 = '单引号很适合创建字符串字面量。';
var s2 = "双引号同样好用。";
var s3 = '很容易对字符串分隔符进行转义。';
var s4 = "使用另一种分隔符甚至更简单。";
你可以使用 ${expression}
将表达式的值放入字符串中。如果表达式是一个标识符,可以省略 {}
。为了得到与对象对应的字符串,Dart 会调用对象的 toString()
方法。
dart
var s = '字符串插值';
assert(
'Dart 有 $s,非常方便。' ==
'Dart 有字符串插值,'
'非常方便。',
);
assert(
'这值得大写。'
'${s.toUpperCase()} 非常方便!' ==
'这值得大写。'
'字符串插值 非常方便!',
);
注意: ==
运算符用于测试两个对象是否等价。如果两个字符串包含相同的代码单元序列,则它们是等价的。
你可以使用相邻的字符串字面量或 +
运算符来连接字符串:
dart
var s1 =
'字符串 '
'连接'
" 即使跨换行符也能正常工作。";
assert(
s1 ==
'字符串连接即使跨 '
'换行符也能正常工作。',
);
var s2 = '使用 + 运算符 ' + '也可以。';
assert(s2 == '使用 + 运算符 也可以。');
要创建多行字符串,可以使用单引号或双引号的三引号形式:
dart
var s1 = '''
你可以像这样
创建多行字符串。
''';
var s2 = """这也是一个
多行字符串。""";
你可以在字符串前加 r
来创建“原始”字符串:
dart
var s = r'在原始字符串中,甚至 \n 也不会得到特殊处理。';
有关如何在字符串中表示 Unicode 字符的详细信息,请参阅“符文和字形簇”。
只要任何插值表达式是求值为 null
或数字、字符串或布尔值的编译时常量,字符串字面量就是编译时常量。
dart
// 这些可以用在常量字符串中。
const aConstNum = 0;
const aConstBool = true;
const aConstString = '一个常量字符串';
// 这些不能用在常量字符串中。
var aNum = 0;
var aBool = true;
var aString = '一个字符串';
const aConstList = [1, 2, 3];
const validConstString = '$aConstNum $aConstBool $aConstString';
// const invalidConstString = '$aNum $aBool $aString $aConstList';
有关使用字符串的更多信息,请查看“字符串和正则表达式”。
布尔值
为了表示布尔值,Dart 有一个名为 bool
的类型。只有两个对象的类型是 bool
:布尔值字面量 true
和 false
,它们都是编译时常量。
Dart 的类型安全意味着你不能使用像 if (nonbooleanValue)
或 assert (nonbooleanValue)
这样的代码。相反,要显式地检查值,如下所示:
dart
// 检查空字符串。
var fullName = '';
assert(fullName.isEmpty);
// 检查是否为零。
var hitPoints = 0;
assert(hitPoints == 0);
// 检查是否为 null。
var unicorn = null;
assert(unicorn == null);
// 检查是否为 NaN。
var iMeantToDoThis = 0 / 0;
assert(iMeantToDoThis.isNaN);
符文和字形簇
在 Dart 中,runes
用于展示字符串的 Unicode 代码点。你可以使用 characters
包来查看或操作用户感知的字符,也称为 Unicode(扩展)字形簇。
Unicode 为世界上所有书写系统中使用的每个字母、数字和符号定义了唯一的数值。因为 Dart 字符串是 UTF - 16 代码单元的序列,所以在字符串中表示 Unicode 代码点需要特殊的语法。通常表示 Unicode 代码点的方式是 \uXXXX
,其中 XXXX
是一个 4 位十六进制值。例如,心形字符(♥)是 \u2665
。要指定多于或少于 4 位十六进制数字,可将值放在花括号中。例如,大笑表情符号(😆)是 \u{1f606}
。
如果你需要读取或写入单个 Unicode 字符,可以使用 characters
包为 String
定义的 characters
getter。返回的 Characters
对象是一个字符串,以字形簇序列的形式呈现。以下是使用 characters
API 的示例:
dart
import 'package:characters/characters.dart';
void main() {
var hi = 'Hi 🇩🇰';
print(hi);
print('字符串的末尾: ${hi.substring(hi.length - 1)}');
print('最后一个字符: ${hi.characters.last}');
}
根据你的环境,输出大致如下:
$ dart run bin/main.dart
Hi 🇩🇰
字符串的末尾: ???
最后一个字符: 🇩🇰
有关使用 characters
包操作字符串的详细信息,请参阅 characters
包的示例和 API 参考。
符号
Symbol
对象表示 Dart 程序中声明的运算符或标识符。你可能永远不需要使用符号,但对于那些按名称引用标识符的 API 来说,它们非常有用,因为代码压缩会改变标识符的名称,但不会改变标识符的符号。
要获取标识符的符号,可以使用符号字面量,即 #
后面跟标识符:
#radix
#bar
符号字面量是编译时常量。
记录
版本说明
记录(Records)需要至少 3.0 的语言版本。
记录是一种匿名的、不可变的聚合类型。和其他集合类型一样,它们允许你将多个对象捆绑成一个单一对象。但与其他集合类型不同的是,记录具有固定大小、可以包含不同类型的元素,并且是带类型的。
记录是真实的值;你可以将它们存储在变量中、嵌套使用、在函数之间传递,还能将它们存储在列表、映射和集合等数据结构中。
记录语法
记录表达式是用逗号分隔的命名或位置字段列表,用括号括起来:
dart
var record = ('first', a: 2, b: true, 'last');
记录类型注解是用逗号分隔的类型列表,用括号括起来。你可以使用记录类型注解来定义返回类型和参数类型。例如,以下 (int, int)
语句就是记录类型注解:
dart
(int, int) swap((int, int) record) {
var (a, b) = record;
return (b, a);
}
记录表达式和类型注解中的字段与函数中的参数和实参的工作方式类似。位置字段直接放在括号内:
dart
// 变量声明中的记录类型注解:
(String, int) record;
// 用记录表达式初始化它:
record = ('A string', 123);
在记录类型注解中,命名字段放在用花括号分隔的类型和名称对的部分,位于所有位置字段之后。在记录表达式中,名称放在每个字段值之前,后面跟一个冒号:
dart
// 变量声明中的记录类型注解:
({int a, bool b}) record;
// 用记录表达式初始化它:
record = (a: 123, b: true);
记录类型中命名字段的名称是记录类型定义(即其结构)的一部分。两个具有不同命名字段名称的记录具有不同的类型:
dart
({int a, int b}) recordAB = (a: 1, b: 2);
({int x, int y}) recordXY = (x: 3, y: 4);
// 编译错误!这些记录的类型不同。
// recordAB = recordXY;
在记录类型注解中,你也可以为位置字段命名,但这些名称仅用于文档目的,不会影响记录的类型:
dart
(int a, int b) recordAB = (1, 2);
(int x, int y) recordXY = (3, 4);
recordAB = recordXY; // 可以。
这类似于函数声明或函数类型定义中的位置参数可以有名称,但这些名称不会影响函数的签名。
更多信息和示例,请查看“记录类型”和“记录相等性”。
记录字段
记录字段可以通过内置的 getter 访问。记录是不可变的,因此字段没有 setter。
命名字段会暴露同名的 getter。位置字段会暴露名为 $<位置>
的 getter,会跳过命名字段:
dart
var record = ('first', a: 2, b: true, 'last');
print(record.$1); // 输出 'first'
print(record.a); // 输出 2
print(record.b); // 输出 true
print(record.$2); // 输出 'last'
若要进一步简化记录字段的访问,请查看“模式”页面。
记录类型
单个记录类型没有类型声明。记录是基于其字段类型进行结构化类型定义的。记录的结构(其字段集、字段类型以及可能存在的字段名称)唯一确定了记录的类型。
记录中的每个字段都有自己的类型。同一记录中的字段类型可以不同。类型系统在从记录中访问每个字段时都能感知其类型:
dart
(num, Object) pair = (42, 'a');
var first = pair.$1; // 静态类型为 `num`,运行时类型为 `int`。
var second = pair.$2; // 静态类型为 `Object`,运行时类型为 `String`。
考虑两个不相关的库创建了具有相同字段集的记录。即使这两个库彼此没有关联,类型系统也会认为这些记录是相同的类型。
记录相等性
如果两个记录具有相同的结构(字段集),并且它们对应的字段具有相同的值,那么这两个记录相等。由于命名字段的顺序不是记录结构的一部分,因此命名字段的顺序不影响相等性。
例如:
dart
(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);
print(point == color); // 输出 'true'。
dart
({int x, int y, int z}) point = (x: 1, y: 2, z: 3);
({int r, int g, int b}) color = (r: 1, g: 2, b: 3);
print(point == color); // 输出 'false'。提示:对不相关类型使用了相等比较。
记录会根据其字段结构自动定义 hashCode
和 ==
方法。
多返回值
记录允许函数将多个值捆绑在一起返回。要从返回值中获取记录值,可以使用模式匹配将值解构到局部变量中。
dart
// 在记录中返回多个值:
(String name, int age) userInfo(Map<String, dynamic> json) {
return (json['name'] as String, json['age'] as int);
}
final json = <String, dynamic>{'name': 'Dash', 'age': 10, 'color': 'blue'};
// 使用带位置字段的记录模式进行解构:
var (name, age) = userInfo(json);
/* 等同于:
var info = userInfo(json);
var name = info.$1;
var age = info.$2;
*/
你也可以使用命名字段对记录进行解构,使用冒号 :
语法,关于这一点你可以在“模式类型”页面了解更多:
dart
({String name, int age}) userInfo(Map<String, dynamic> json)
// ···
// 使用带命名字段的记录模式进行解构:
final (:name, :age) = userInfo(json);
你可以不使用记录从函数中返回多个值,但其他方法存在一些缺点。例如,创建一个类会更冗长,而使用 List
或 Map
等其他集合类型会失去类型安全性。
注意: 记录的多返回值和异构类型特性支持对不同类型的 Future
进行并行处理,你可以在 dart:async
文档中了解相关内容。
作为简单数据结构的记录
记录仅用于保存数据。当你只需要这个功能时,它们可以直接使用,而且无需声明任何新类,使用起来很方便。对于所有具有相同结构的简单数据元组列表,记录列表是最直接的表示方式。
例如,以下是一个“按钮定义”列表:
dart
final buttons = [
(
label: "Button I",
icon: const Icon(Icons.upload_file),
onPressed: () => print("Action -> Button I"),
),
(
label: "Button II",
icon: const Icon(Icons.info),
onPressed: () => print("Action -> Button II"),
)
];
这段代码可以直接编写,无需任何额外的声明。
记录和类型定义(typedefs)
你可以选择使用类型定义为记录类型本身命名,然后使用这个名称而不是写出完整的记录类型。这种方法允许你声明某些字段可以为 null
(?
),即使列表中当前的条目都没有 null
值。
dart
typedef ButtonItem = ({String label, Icon icon, void Function()? onPressed});
final List<ButtonItem> buttons = [
// ...
];
由于记录类型是结构化类型,给像 ButtonItem
这样的名称只是引入了一个别名,使得引用结构化类型 ({String label, Icon icon, void Function()? onPressed})
更加方便。
让所有代码通过别名引用记录类型,这样以后更改记录的实现时,就无需更新每个引用。
代码处理给定的按钮定义的方式与处理简单类实例的方式相同:
dart
List<Container> widget = [
for (var button in buttons)
Container(
margin: const EdgeInsets.all(4.0),
child: OutlinedButton.icon(
onPressed: button.onPressed,
icon: button.icon,
label: Text(button.label),
),
),
];
你甚至可以决定以后将记录类型更改为类类型以添加方法:
dart
class ButtonItem {
final String label;
final Icon icon;
final void Function()? onPressed;
ButtonItem({required this.label, required this.icon, this.onPressed});
bool get hasOnpressed => onPressed != null;
}
或者更改为扩展类型:
dart
extension type ButtonItem._(({String label, Icon icon, void Function()? onPressed}) _) {
String get label => _.label;
Icon get icon => _.icon;
void Function()? get onPressed => _.onPressed;
ButtonItem({required String label, required Icon icon, void Function()? onPressed})
: this._((label: label, icon: icon, onPressed: onPressed));
bool get hasOnpressed => _.onPressed != null;
}
然后使用该类型的构造函数创建按钮定义列表:
dart
final List<ButtonItem> buttons = [
ButtonItem(
label: "Button I",
icon: const Icon(Icons.upload_file),
onPressed: () => print("Action -> Button I"),
),
ButtonItem(
label: "Button II",
icon: const Icon(Icons.info),
onPressed: () => print("Action -> Button II"),
)
];
同样,在整个过程中都无需更改使用该列表的代码。
更改任何类型都需要使用该类型的代码非常小心,不要做出不合理的假设。类型别名对于使用它作为引用的代码来说,并不能提供任何保护或保证被别名化的值是一个记录。扩展类型也提供很少的保护。只有类可以提供完整的抽象和封装。
集合
Dart 对列表(list)、集合(set)和映射(map)集合提供了内置支持。若要了解更多关于配置集合所包含类型的信息,请查阅“泛型”相关内容。
列表
在几乎所有编程语言中,数组(即对象的有序组)可能是最常见的集合。在 Dart 里,数组就是 List
对象,所以大多数人直接称它们为列表。
Dart 列表字面量由用逗号分隔的表达式或值列表组成,并用方括号 []
括起来。以下是一个简单的 Dart 列表:
dart
var list = [1, 2, 3];
注意: Dart 会推断 list
的类型为 List<int>
。如果你试图向这个列表中添加非整数对象,分析器或运行时会抛出错误。更多信息请阅读“类型推断”相关内容。
在 Dart 集合字面量的最后一项后面可以添加逗号。这个尾随逗号不会影响集合,但可以帮助避免复制粘贴错误。
dart
var list = ['Car', 'Boat', 'Plane'];
列表使用从 0 开始的索引,其中 0 是第一个值的索引,list.length - 1
是最后一个值的索引。你可以使用 .length
属性获取列表的长度,并使用下标运算符 []
访问列表的值:
dart
var list = [1, 2, 3];
assert(list.length == 3);
assert(list[1] == 2);
list[1] = 1;
assert(list[1] == 1);
要创建一个编译时常量列表,在列表字面量前添加 const
:
dart
var constantList = const [1, 2, 3];
// constantList[1] = 1; // 这一行会导致错误。
有关列表的更多信息,请参考 dart:core
文档中的“列表”部分。
集合
Dart 中的集合是一个无序的唯一元素集合。Dart 通过集合字面量和 Set
类型来支持集合。
以下是一个使用集合字面量创建的简单 Dart 集合:
dart
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
注意: Dart 会推断 halogens
的类型为 Set<String>
。如果你试图向集合中添加错误类型的值,分析器或运行时会抛出错误。更多信息请阅读“类型推断”相关内容。
要创建一个空集合,可以使用带有类型参数的 {}
,或者将 {}
赋值给 Set
类型的变量:
dart
var names = <String>{};
// Set<String> names = {}; // 这样写也可以。
// var names = {}; // 这会创建一个映射,而不是集合。
集合还是映射? 映射字面量的语法与集合字面量的语法相似。由于映射字面量出现得更早,{}
默认是 Map
类型。如果你忘记在 {}
或赋值给它的变量上添加类型注解,Dart 会创建一个 Map<dynamic, dynamic>
类型的对象。
使用 add()
或 addAll()
方法向现有集合中添加元素:
dart
var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);
使用 .length
获取集合中元素的数量:
dart
var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);
assert(elements.length == 5);
要创建一个编译时常量集合,在集合字面量前添加 const
:
dart
final constantSet = const {
'fluorine',
'chlorine',
'bromine',
'iodine',
'astatine',
};
// constantSet.add('helium'); // 这一行会导致错误。
有关集合的更多信息,请参考 dart:core
文档中的“集合”部分。
映射
一般来说,映射是一种将键和值关联起来的对象。键和值都可以是任何类型的对象。每个键只能出现一次,但同一个值可以使用多次。Dart 通过映射字面量和 Map
类型来支持映射。
以下是使用映射字面量创建的几个简单 Dart 映射:
dart
var gifts = {
// 键: 值
'first': 'partridge',
'second': 'turtledoves',
'fifth': 'golden rings',
};
var nobleGases = {2: 'helium', 10: 'neon', 18: 'argon'};
注意: Dart 会推断 gifts
的类型为 Map<String, String>
,nobleGases
的类型为 Map<int, String>
。如果你试图向任一映射中添加错误类型的值,分析器或运行时会抛出错误。更多信息请阅读“类型推断”相关内容。
你也可以使用 Map
构造函数创建相同的对象:
dart
var gifts = Map<String, String>();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';
var nobleGases = Map<int, String>();
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';
注意: 如果你来自 C# 或 Java 等语言,可能会期望看到 new Map()
而不是 Map()
。在 Dart 中,new
关键字是可选的。详细信息请参阅“使用构造函数”。
使用下标赋值运算符 []=
向现有映射中添加新的键值对:
dart
var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds'; // 添加一个键值对
使用下标运算符 []
从映射中检索值:
dart
var gifts = {'first': 'partridge'};
assert(gifts['first'] == 'partridge');
如果你查找的键不在映射中,将返回 null
:
dart
var gifts = {'first': 'partridge'};
assert(gifts['fifth'] == null);
使用 .length
获取映射中键值对的数量:
dart
var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds';
assert(gifts.length == 2);
要创建一个编译时常量映射,在映射字面量前添加 const
:
dart
final constantMap = const {2: 'helium', 10: 'neon', 18: 'argon'};
// constantMap[2] = 'Helium'; // 这一行会导致错误。
有关映射的更多信息,请参考 dart:core
文档中的“映射”部分。
运算符
展开运算符
Dart 在列表、映射和集合字面量中支持展开运算符 ...
和空感知展开运算符 ...?
。展开运算符提供了一种简洁的方式将多个值插入到集合中。
例如,你可以使用展开运算符 ...
将一个列表的所有值插入到另一个列表中:
dart
var list = [1, 2, 3];
var list2 = [0, ...list];
assert(list2.length == 4);
如果展开运算符右边的表达式可能为 null
,你可以使用空感知展开运算符 ...?
来避免异常:
dart
var list2 = [0, ...?list];
assert(list2.length == 1);
有关使用展开运算符的更多详细信息和示例,请参阅“展开运算符提案”。
控制流运算符
Dart 在列表、映射和集合字面量中提供了集合 if
和集合 for
。你可以使用这些运算符通过条件判断(if
)和重复操作(for
)来构建集合。
以下是一个使用集合 if
创建包含三个或四个元素的列表的示例:
dart
var nav = ['Home', 'Furniture', 'Plants', if (promoActive) 'Outlet'];
Dart 还支持在集合字面量中使用 if-case
:
dart
var nav = ['Home', 'Furniture', 'Plants', if (login case 'Manager') 'Inventory'];
以下是一个使用集合 for
在将列表元素添加到另一个列表之前对其进行处理的示例:
dart
var listOfInts = [1, 2, 3];
var listOfStrings = ['#0', for (var i in listOfInts) '#$i'];
assert(listOfStrings[1] == '#1');
有关使用集合 if
和 for
的更多详细信息和示例,请参阅“控制流集合提案”。
泛型
如果你查看基本数组类型 List
的 API 文档,会发现它实际上是 List<E>
类型。<...>
符号表明 List
是一个泛型(或参数化)类型,即带有形式类型参数的类型。按照惯例,大多数类型变量使用单字母名称,如 E
、T
、S
、K
和 V
。
为什么使用泛型?
泛型通常是实现类型安全所必需的,但它们的好处不止于让代码能够运行:
- 正确指定泛型类型可以生成更好的代码。
- 可以使用泛型减少代码重复。
如果你希望一个列表只包含字符串,可以将其声明为 List<String>
(读作“字符串列表”)。这样,你、其他程序员以及开发工具就能检测出向该列表赋值非字符串类型的元素可能是个错误。以下是一个示例:
dart
✗ 静态分析:失败
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // 错误
使用泛型的另一个原因是减少代码重复。泛型允许你在多种类型之间共享单个接口和实现,同时还能利用静态分析。例如,假设你创建了一个用于缓存对象的接口:
dart
abstract class ObjectCache {
Object getByKey(String key);
void setByKey(String key, Object value);
}
后来你发现需要这个接口的字符串专用版本,于是又创建了另一个接口:
dart
abstract class StringCache {
String getByKey(String key);
void setByKey(String key, String value);
}
再后来,你可能又想创建该接口的数字专用版本……你应该能明白这种情况。
泛型类型可以让你免去创建所有这些接口的麻烦。相反,你可以创建一个带有类型参数的单一接口:
dart
abstract class Cache<T> {
T getByKey(String key);
void setByKey(String key, T value);
}
在这段代码中,T
是占位类型。可以把它看作是开发者稍后会定义的类型的占位符。
使用集合字面量
列表、集合和映射字面量可以进行参数化。参数化字面量与你之前见过的字面量类似,只是在左括号前添加 <type>
(用于列表和集合)或 <keyType, valueType>
(用于映射)。以下是使用类型化字面量的示例:
dart
var names = <String>['Seth', 'Kathy', 'Lars'];
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
var pages = <String, String>{
'index.html': '主页',
'robots.txt': '给网络爬虫的提示',
'humans.txt': '我们是人,不是机器',
};
在构造函数中使用参数化类型
使用构造函数时,若要指定一个或多个类型,可将这些类型放在类名后面的尖括号 <...>
中。例如:
dart
var nameSet = Set<String>.of(names);
以下代码创建了一个键为整数、值为 View
类型的 SplayTreeMap
:
dart
var views = SplayTreeMap<int, View>();
泛型集合及其包含的类型
Dart 的泛型类型是具体化的,这意味着它们在运行时会携带自身的类型信息。例如,你可以测试集合的类型:
dart
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true
注意: 相比之下,Java 中的泛型使用类型擦除,这意味着泛型类型参数在运行时会被移除。在 Java 中,你可以测试一个对象是否为 List
,但无法测试它是否为 List<String>
。
限制参数化类型
实现泛型类型时,你可能希望限制作为参数提供的类型,使参数必须是特定类型的子类型。这种限制称为边界。可以使用 extends
来实现。
一个常见的用例是通过使类型成为 Object
的子类型(而非默认的 Object?
)来确保该类型是非可空的。
dart
class Foo<T extends Object> {
// 任何提供给 Foo 的 T 类型都必须是非可空的。
}
除了 Object
,你还可以将 extends
与其他类型一起使用。以下是一个扩展 SomeBaseClass
的示例,这样就可以在 T
类型的对象上调用 SomeBaseClass
的成员:
dart
class Foo<T extends SomeBaseClass> {
// 实现代码放在这里...
String toString() => "Instance of 'Foo<$T>'";
}
class Extender extends SomeBaseClass {
...
}
使用 SomeBaseClass
或其任何子类型作为泛型参数是可以的:
dart
var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();
不指定泛型参数也是可以的:
dart
var foo = Foo();
print(foo); // Instance of 'Foo<SomeBaseClass>'
指定任何非 SomeBaseClass
类型会导致错误:
dart
✗ 静态分析:失败
var foo = Foo<Object>();
自引用类型参数限制(F 边界)
使用边界来限制参数类型时,你可以将边界引用回类型参数本身。这会创建一个自引用约束,即 F 边界。例如:
dart
abstract interface class Comparable<T> {
int compareTo(T o);
}
int compareAndOffset<T extends Comparable<T>>(T t1, T t2) =>
t1.compareTo(t2) + 1;
class A implements Comparable<A> {
@override
int compareTo(A other) => /*...实现代码...*/ 0;
}
var useIt = compareAndOffset(A(), A());
F 边界 T extends Comparable<T>
意味着 T
必须能与自身进行比较。因此,A
只能与相同类型的其他实例进行比较。
使用泛型方法
方法和函数也允许使用类型参数:
dart
T first<T>(List<T> ts) {
// 进行一些初始工作或错误检查,然后...
T tmp = ts[0];
// 进行一些额外的检查或处理...
return tmp;
}
这里 first
方法上的泛型类型参数 <T>
允许你在多个地方使用类型参数 T
:
- 在函数的返回类型(
T
)中。 - 在参数的类型(
List<T>
)中。 - 在局部变量的类型(
T tmp
)中。
类型别名
类型别名(通常称为 typedef
,因为它是使用 typedef
关键字声明的)是一种简洁的类型引用方式。以下是一个声明并使用名为 IntList
的类型别名的示例:
dart
typedef IntList = List<int>;
IntList il = [1, 2, 3];
类型别名可以有类型参数:
dart
typedef ListMapper<X> = Map<X, List<X>>;
Map<String, List<String>> m1 = {}; // 冗长的写法。
ListMapper<String> m2 = {}; // 同样的意思,但更简短清晰。
版本说明
在 2.13 版本之前,typedef
仅限于函数类型。使用新的 typedef
功能需要至少 2.13 的语言版本。
我们建议在大多数情况下,对于函数使用内联函数类型而不是 typedef
。不过,函数类型别名仍然有其用处:
dart
typedef Compare<T> = int Function(T a, T b);
int sort(int a, int b) => a - b;
void main() {
assert(sort is Compare<int>); // 正确!
}
类型系统
Dart 语言是类型安全的:它结合使用静态类型检查和运行时检查,以确保变量的值始终与其静态类型匹配,这有时也被称为健全类型系统。尽管类型是必需的,但由于类型推断,类型注解是可选的。
静态类型检查的一个好处是能够使用 Dart 的静态分析器在编译时发现 bug。
你可以通过为泛型类添加类型注解来修复大多数静态分析错误。最常见的泛型类是集合类型 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
处产生一个类型错误(已在上文高亮显示):
错误 - 参数类型 'List<dynamic>' 不能赋值给参数类型 'List<int>'。 - argument_type_not_assignable
该错误突出显示了从 List<dynamic>
到 List<int>
的不安全隐式转换。list
变量的静态类型是 List<dynamic>
,这是因为初始化声明 var list = []
没有为分析器提供足够的信息,使其无法推断出比 dynamic
更具体的类型参数。printInts()
函数期望的参数类型是 List<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
类型会在运行时抛出错误。
健全性的好处
健全的类型系统有以下几个好处:
- 在编译时揭示与类型相关的 bug:健全的类型系统要求代码在类型方面明确无误,因此那些在运行时可能难以发现的与类型相关的 bug 会在编译时被揭示出来。
- 代码更易读:由于你可以确信一个值确实具有指定的类型,因此代码更易于阅读。在健全的 Dart 代码中,类型不会产生误导。
- 代码更易维护:使用健全的类型系统,当你修改一段代码时,类型系统可以提醒你其他哪些代码可能因此而失效。
- 更高效的提前编译(AOT):虽然不使用类型也可以进行提前编译,但生成的代码效率会低很多。
通过静态分析的提示
大多数静态类型规则都很容易理解。以下是一些不太明显的规则:
- 重写方法时使用健全的返回类型
- 重写方法时使用健全的参数类型
- 不要将动态列表用作类型化列表
让我们结合以下类型层次结构的示例,详细了解这些规则:
有一个动物的类型层次结构,其中超类型是 Animal
,子类型有 Alligator
、Cat
和 HoneyBadger
,Cat
还有子类型 Lion
和 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 => ...
}
重写方法时使用健全的参数类型
被重写方法的参数类型必须与超类中对应参数的类型相同,或者是其超类型。不要通过将参数类型替换为原始参数的子类型来“收紧”参数类型。
注意: 如果你有合理的理由使用子类型,可以使用 covariant
关键字。
考虑 Animal
类中的 chase(Animal)
方法:
dart
class Animal {
void chase(Animal a) {
...
}
Animal get parent => ...
}
chase()
方法接受一个 Animal
类型的参数。HoneyBadger
会追逐任何东西,因此重写 chase()
方法使其接受任何类型的对象(Object
)是可以的。
dart
✔ 静态分析:成功
class HoneyBadger extends Animal {
@override
void chase(Object a) {
...
}
@override
Animal get parent => ...
}
以下代码将 chase()
方法的参数类型从 Animal
收紧为 Animal
的子类 Mouse
:
dart
✗ 静态分析:失败
class Mouse extends Animal {
...
}
class Cat extends Animal {
@override
void chase(Mouse a) {
...
}
}
这段代码不是类型安全的,因为这样就有可能定义一只猫并让它去追逐一只短吻鳄:
dart
Animal a = Cat();
a.chase(Alligator()); // 既不是类型安全的,也不是符合猫的行为逻辑的。
不要将动态列表用作类型化列表
当你希望列表中包含不同类型的元素时,动态列表很有用。但是,你不能将动态列表用作类型化列表。
此规则也适用于泛型类型的实例。
以下代码创建了一个 Dog
类型的动态列表,并将其赋值给 Cat
类型的列表,这会在静态分析期间产生错误。
dart
✗ 静态分析:失败
void main() {
List<Cat> foo = <dynamic>[Dog()]; // 错误
List<dynamic> bar = <dynamic>[Dog(), Cat()]; // 可以
}
运行时检查
运行时检查用于处理编译时无法检测到的类型安全问题。
例如,以下代码在运行时会抛出异常,因为将 Dog
列表转换为 Cat
列表是错误的:
dart
✗ 运行时:失败
void main() {
List<Animal> animals = <Dog>[Dog()];
List<Cat> cats = animals as List<Cat>;
}
类型推断
分析器可以推断字段、方法、局部变量和大多数泛型类型参数的类型。当分析器没有足够的信息来推断特定类型时,它会使用 dynamic
类型。
以下是一个关于泛型类型推断如何工作的示例。在这个示例中,一个名为 arguments
的变量持有一个映射,该映射将字符串键与各种类型的值配对。
如果你显式指定变量的类型,可以这样写:
dart
Map<String, dynamic> arguments = {'argA': 'hello', 'argB': 42};
或者,你可以使用 var
或 final
,让 Dart 推断类型:
dart
var arguments = {'argA': 'hello', 'argB': 42}; // Map<String, Object>
映射字面量会根据其条目推断自身的类型,然后变量会根据映射字面量的类型推断自身的类型。在这个映射中,键都是字符串,但值的类型不同(String
和 int
,它们的上界是 Object
)。因此,映射字面量的类型是 Map<String, Object>
,arguments
变量的类型也是如此。
字段和方法推断
没有指定类型且重写了超类中的字段或方法的字段或方法,会继承超类中该方法或字段的类型。
没有声明或继承类型但使用初始值声明的字段,会根据初始值推断类型。
静态字段推断
静态字段和变量的类型会从其初始化器推断得出。请注意,如果推断过程中遇到循环(即推断变量的类型依赖于知道该变量的类型),推断将会失败。
局部变量推断
局部变量的类型(如果有)会从其初始化器推断得出。后续的赋值不会被考虑在内。这可能意味着推断出的类型可能过于精确。如果是这样,你可以添加类型注解。
dart
✗ 静态分析:失败
var x = 3; // x 被推断为 int 类型。
x = 4.0;
dart
✔ 静态分析:成功
num y = 3; // num 类型可以是 double 或 int。
y = 4.0;
类型参数推断
构造函数调用和泛型方法调用的类型参数是根据出现上下文的向下信息以及构造函数或泛型方法的参数的向上信息综合推断得出的。如果推断结果不符合你的预期,你始终可以显式指定类型参数。
dart
✔ 静态分析:成功
// 推断结果就像你写了 <int>[] 一样。
List<int> listOfInt = [];
// 推断结果就像你写了 <double>[3.0] 一样。
var listOfDouble = [3.0];
// 推断为 Iterable<int> 类型。
var ints = listOfDouble.map((x) => x.toInt());
在最后一个示例中,使用向下信息将 x
推断为 double
类型。使用向上信息将闭包的返回类型推断为 int
类型。Dart 在推断 map()
方法的类型参数 <int>
时,将此返回类型作为向上信息使用。
使用边界进行推断
版本说明
使用边界进行推断需要至少 3.7.0 的语言版本。
借助使用边界进行推断这一特性,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()); // 可以。
// 可以。如果不使用边界,依赖尽力近似推断的算法在检测到 `C` 不是 `A<C>` 的子类型后会推断失败。
f(C());
f<B>(C()); // 可以。
}
以下是一个更实际的示例,使用了 Dart 中常见的类型,如 int
或 num
:
dart
X max<X extends Comparable<X>>(X x1, X x2) => x1.compareTo(x2) > 0 ? x1 : x2;
void main() {
// 使用该特性时推断为 `max<num>(3, 7)`,没有该特性则会失败。
max(3, 7);
}
使用边界进行推断时,Dart 可以解构类型参数,从泛型类型参数的边界中提取类型信息。这使得下面示例中的 f
等函数能够同时保留特定的可迭代类型(List
或 Set
)和元素类型。在使用边界进行推断之前,如果不损失类型安全性或特定类型信息,这是不可能实现的。
dart
(X, Y) f<X extends Iterable<Y>, Y>(X x) => (x, x.first);
void main() {
var (myList, myInt) = f1();
myInt.whatever; // 编译时错误,`myInt` 的类型是 `int`。
var (mySet, myString) = f1({'Hello!'});
mySet.union({}); // 可以,`mySet` 的类型是 `Set<String>`。
}
如果不使用边界进行推断,myInt
的类型将是 dynamic
。之前的推断算法不会在编译时捕获到错误的表达式 myInt.whatever
,而是会在运行时抛出错误。相反,如果不使用边界进行推断,mySet.union({})
将是一个编译时错误,因为之前的算法无法保留 mySet
是 Set
类型的信息。
有关使用边界进行推断算法的更多信息,请阅读设计文档。
类型替换
当你重写一个方法时,你是在用一种可能具有新类型的东西(新方法中的)替换另一种类型的东西(旧方法中的)。同样,当你向函数传递参数时,你是在用具有另一种类型的东西(实际参数)替换具有某种类型的东西(声明了类型的参数)。那么,什么时候可以用子类型或超类型的东西替换具有某种类型的东西呢?
在进行类型替换时,从消费者和生产者的角度来思考会很有帮助。消费者吸收某种类型,而生产者生成某种类型。
你可以用超类型替换消费者的类型,用子类型替换生产者的类型。
让我们来看一些简单类型赋值和泛型类型赋值的示例。
简单类型赋值
在将对象赋值给对象时,什么时候可以用不同的类型替换一种类型呢?答案取决于该对象是消费者还是生产者。
考虑以下类型层次结构:
有一个动物的类型层次结构,其中超类型是 Animal
,子类型有 Alligator
、Cat
和 HoneyBadger
,Cat
还有子类型 Lion
和 MaineCoon
。
考虑以下简单的赋值,其中 Cat c
是消费者,Cat()
是生产者:
dart
Cat c = Cat();
在消费者位置,用可以接受任何类型的东西(Animal
)替换接受特定类型(Cat
)的东西是安全的,因此将 Cat c
替换为 Animal c
是允许的,因为 Animal
是 Cat
的超类型。
dart
✔ 静态分析:成功
Animal c = Cat();
但是,将 Cat c
替换为 MaineCoon c
会破坏类型安全,因为超类可能会提供具有不同行为的 Cat
类型,例如 Lion
:
dart
✗ 静态分析:失败
MaineCoon c = Cat();
在生产者位置,用更具体的类型(MaineCoon
)替换生成某种类型(Cat
)的东西是安全的。因此,以下赋值是允许的:
dart
✔ 静态分析:成功
Cat c = MaineCoon();
泛型类型赋值
泛型类型的规则是否相同呢?是的。考虑动物列表的层次结构——Cat
列表是 Animal
列表的子类型,也是 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;
这个赋值无法通过静态分析,因为它会创建一个隐式的向下转换,而从非动态类型(如 Animal
)进行隐式向下转换是不允许的。
要使这种类型的代码通过静态分析,你可以使用显式转换。
dart
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals as List<Cat>;
不过,显式转换在运行时仍然可能失败,这取决于被转换列表(myAnimals
)的实际类型。
方法
重写方法时仍然适用生产者和消费者规则。例如:
(这里展示 Animal
类,chase
方法作为消费者,parent
getter
方法作为生产者)
对于消费者(如 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
和字段上也受支持。