文章

Dart类型

本文介绍了dart的类型,包括基础类型,集合,记录,泛型,别名,类型体系。

Dart语言详解

Dart 语言支持以下类型:

  • Numbers(intdouble)
  • Strings (String)
  • Booleans (bool)
  • Records ((value1, value2))
  • Lists (List, also known as arrays)
  • Sets (Set)
  • Maps (Map)
  • Runes (Runes; often replaced by the characters API)
  • Symbols (Symbol)
  • The value null (Null)

这些类型都可以被初始化为字面量。 例如, 'this is a string' 是一个字符串的字面量, true 是一个布尔的字面量。

由于 Dart 中的每个变量都引用一个对象(一个类的实例),所以变量可以使用构造函数进行初始化。 一些内置类型拥有自己的构造函数。 例如, 通过 Map() 来构造一个 map 变量。

其他一些类型在 Dart 语言中也有特殊作用:

  • Object:除Null之外的所有 Dart 类的超类。
  • Enum:所有枚举的超类。
  • FutureStream:用于异步支持。
  • Iterable:用于for-in 循环和同步生成器函数。
  • Never:表示表达式永远无法成功完成求值。最常用于总是抛出异常的函数。
  • dynamic:表示要禁用静态检查。通常应使用ObjectObject?代替。
  • void:表示从未使用过该值。通常用作返回类型。

ObjectObject?Null和类在类层次结构中具有特殊角色。

基本类型

Number

Dart 语言的 Number 有两种类型:

  • int 整数值不大于64位, 具体取决于平台。
    • 在 Dart VM 上, 值的范围从 $-2 ^{63}$ 到 $2 ^{63} - 1$.
    • 在 Web 上,整数值表示为 JavaScript 数字(没有小数部分的 64 位浮点值), 值的范围从$-2 ^{53}$ 到 $2 ^{53} - 1$.
  • double 64位(双精度)浮点数,依据 IEEE 754 标准。

int double 都是 num的子类。 num 类型包括基本运算 +, -, /, 和 *, 以及 abs()ceil(), 和 floor(), 等函数方法。 (按位运算符,例如»,定义在 int 类中。) 如果 num 及子类找不到你想要的方法, 尝试查找使用 dart:math 库。

整数类型不包含小数点。

1
2
var x = 1;
var hex = 0xDEADBEEF;

如果数字包含小数,则它是双精度数。

1
2
var y = 1.1;
var exponents = 1.42e5;

还可以将变量声明为 num。如果这样做,变量可以同时具有整数和双精度值。

1
2
num x = 1; // x can have both int and double values
x += 2.5;

从 Dart 2.1 开始,必要的时候 int 字面量会自动转换成 double 类型。

1
double z = 1; // 相当于 double z = 1.0.

版本提示: 在 2.1 之前,在 double 上下文中使用 int 字面量是错误的。

以下是将字符串转换为数字的方法,反之亦然:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// String -> int
var one = int.parse('1');
assert(one == 1);

// String -> double
var onePointOne = double.parse('1.1');
assert(onePointOne == 1.1);

// int -> String
String oneAsString = 1.toString();
assert(oneAsString == '1');

// double -> String
String piAsString = 3.14159.toStringAsFixed(2);
assert(piAsString == '3.14');

int类型指定传统的位移位(<<>>>>>)、补码(~)、与(&)、或(|)和异或(^)运算符,这些运算符对于操作和屏蔽位字段中的标志很有用。

1
2
3
assert((3 << 1) == 6); // 0011 << 1 == 0110
assert((3 >> 1) == 1); // 0011 >> 1 == 0001
assert((3 | 4) == 7); // 0011 | 0100 == 0111

数字类型字面量是编译时常量。 在算术表达式中,只要参与计算的因子是编译时常量, 那么算术表达式的结果也是编译时常量。

1
2
3
const msPerSecond = 1000;
const secondsUntilRetry = 5;
const msUntilRetry = secondsUntilRetry * msPerSecond;

String

Dart 字符串是一组(String对象) UTF-16 单元序列。 字符串通过单引号或者双引号创建。

1
2
3
4
var s1 = 'Single quotes work well for string literals.';
var s2 = "Double quotes work just as well.";
var s3 = 'It\'s easy to escape the string delimiter.';
var s4 = "It's even easier to use the other delimiter.";

字符串可以通过 ${expression} 的方式内嵌表达式。 如果表达式是一个标识符,则 {} 可以省略。 在 Dart 中通过调用就对象的 toString() 方法来得到对象相应的字符串。

1
2
3
4
5
6
7
8
9
var s = 'string interpolation';

assert('Dart has $s, which is very handy.' ==
    'Dart has string interpolation, ' +
        'which is very handy.');
assert('That deserves all caps. ' +
        '${s.toUpperCase()} is very handy!' ==
    'That deserves all caps. ' +
        'STRING INTERPOLATION is very handy!');

提示:如果两个字符串包含相同的代码单元序列,则它们是相等的。

使用相邻的字符串文字或+ 运算符连接字符串:

1
2
3
4
5
6
7
8
9
var s1 = 'String '
    'concatenation'
    " works even over line breaks.";
assert(s1 ==
    'String concatenation works even over '
    'line breaks.');

var s2 = 'The + operator ' + 'works, as well.';
assert(s2 == 'The + operator works, as well.');

使用带有单引号或双引号的三重引号,创建多行字符串:

1
2
3
4
5
6
7
var s1 = '''
You can create
multi-line strings like this one.
''';

var s2 = """This is also a
multi-line string.""";

使用 r 前缀,可以创建 “原始 raw” 字符串:

1
var s = r'In a raw string, not even \n gets special treatment.';

参考 Runes 来了解如何在字符串中表达 Unicode 字符。

一个编译时常量的字面量字符串中,如果存在插值表达式,表达式内容也是编译时常量, 那么该字符串依旧是编译时常量。 插入的常量值类型可以是 null,数值,字符串或布尔值。

1
2
3
4
5
6
7
8
9
10
11
12
13
// const 类型数据
const aConstNum = 0;
const aConstBool = true;
const aConstString = 'a constant string';

// 非 const 类型数据
var aNum = 0;
var aBool = true;
var aString = 'a string';
const aConstList = [1, 2, 3];

const validConstString = '$aConstNum $aConstBool $aConstString'; //const 类型数据
// const invalidConstString = '$aNum $aBool $aString $aConstList'; //非 const 类型数据

更多关于 string 的使用, 参考 字符串和正则表达式.

Boolean

Dart 使用 bool 类型表示布尔值。 Dart 只有字面量 true and false 是布尔类型, 这两个对象都是编译时常量。

Dart 的类型安全意味着不能使用 if (nonbooleanValue) 或者 assert (nonbooleanValue)。 而是应该像下面这样,明确的进行值检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 检查空字符串。
var fullName = '';
assert(fullName.isEmpty);

// 检查 0 值。
var hitPoints = 0;
assert(hitPoints <= 0);

// 检查 null 值。
var unicorn;
assert(unicorn == null);

// 检查 NaN 。
var iMeantToDoThis = 0 / 0;
assert(iMeantToDoThis.isNaN);

Rune

在 Dart 中, Rune 用来表示字符串中的 UTF-32 编码字符。

Unicode 为世界上所有书写系统中使用的每个字母、数字和符号定义了一个唯一的数值。由于 Dart 字符串是 UTF-16 代码单元的序列,因此在字符串中表达 Unicode 代码点需要特殊的语法。

表示 Unicode 编码的常用方法是, \uXXXX, 这里 XXXX 是一个4位的16进制数。 例如,心形符号 (♥) 是 \u2665。 要指定多于或少于 4 位十六进制数字,请将值放在花括号中。例如,笑脸表情符号 (😆) 为\u{1f606}

如果需要读取或写入单个 Unicode 字符,使用characters 包在 String 上定义的characters getter。返回的Characters对象是作为字素簇序列的字符串。以下是使用 characters API 的示例:

1
2
3
4
5
6
7
8
import 'package:characters/characters.dart';

void main() {
  var hi = 'Hi 🇩🇰';
  print(hi);
  print('The end of the string: ${hi.substring(hi.length - 1)}');
  print('The last character: ${hi.characters.last}');
}

String 类有一些属性可以获得 rune 数据。 属性 codeUnitAtcodeUnit 返回16位编码数据。 属性 runes 获取字符串中的 Rune 。

下面是示例演示了 Rune 、 16-bit code units、 和 32-bit code points 之间的关系。

1
2
3
4
5
6
7
8
9
10
main() {
  var clapping = '\u{1f44f}';
  print(clapping);
  print(clapping.codeUnits);
  print(clapping.runes.toList());

  Runes input = new Runes(
      '\u2665  \u{1f605}  \u{1f60e}  \u{1f47b}  \u{1f596}  \u{1f44d}');
  print(new String.fromCharCodes(input));
}

👏 [55357, 56399] [128079] ♥ 😅 😎 👻 🖖 👍

提示: 谨慎使用 list 方式操作 Rune 。 这种方法很容易引发崩溃, 具体原因取决于特定的语言,字符集和操作。 有关更多信息,参考How do I reverse a String in Dart? on Stack Overflow.

Symbol

一个 Symbol 对象表示 Dart 程序中声明的运算符或者标识符。 你也许永远都不需要使用 Symbol ,但要按名称引用标识符的 API 时, Symbol 就非常有用了。 因为代码压缩后会改变标识符的名称,但不会改变标识符的符号。 通过字面量 Symbol ,也就是标识符前面添加一个 # 号,来获取标识符的 Symbol 。

1
2
#radix
#bar

Symbol 字面量是编译时常量。 Symbol 详细请参阅:Dart 反射

集合(Collections)

List

几乎每种编程语言中最常见的集合可能是数组,即有序的对象组。 在 Dart 中, Array 就是 List 对象, 通常称之为 List 。

Dart 中的 List 字面量由逗号分隔的表达式或值列表表示,括在方括号 ( []) 中。 下面是一个 Dart List 的示例:

1
var list = [1, 2, 3];

提示: Dart 推断 list 的类型为 List<int> 。 如果尝试将非整数对象添加到此 List 中, 则分析器或运行时会引发错误。

Lists 的下标索引从 0 开始,第一个元素的索引是 0。 list.length - 1 是最后一个元素的索引。使用属性获取列表的长度,并使用下标运算符 ( ).length访问列表的值:[]

1
2
3
4
5
6
var list = [1, 2, 3];
assert(list.length == 3);
assert(list[1] == 2);

list[1] = 1;
assert(list[1] == 1);

在 List 字面量之前添加 const 关键字,可以定义 List 类型的编译时常量:

1
2
var constantList = const [1, 2, 3];
// constantList[1] = 1; // 取消注释会引起错误。

List的一些常用API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var list = [1, 2, 3];
assert(list[1] == 2);

list.add(value);
list.addAll(iterable);
list.insert(index, element);
list.insertAll(index, iterable);

list.remove(2);
list.removeAt(1);
list.removeRange(0, 5);
list.removeLast();
list.clear();

list.indexOf(element);
list.lastIndexOf(element);

list.removeWhere((item) => item > 3);
list.sort((a, b) => a.compareTo(b));
var l = list.reversed;
list.isNotEmpty;
list.isEmpty;
list.length;

Set

在 Dart 中 Set 是一个元素唯一且无需的集合。 Dart 为 Set 提供了 Set 字面量和 Set 类型。

下面是通过字面量创建 Set 的一个简单示例:

1
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};

Note: Dart 推断 halogens 类型为 Set< String > 。如果尝试为它添加一个 错误类型的值,分析器或执行时会抛出错误。

要创建一个空集,使用前面带有类型参数的 {} ,或者将 {} 赋值给 Set 类型的变量:

1
2
3
var names = <String>{};
// Set<String> names = {}; // 这样也是可以的。
// var names = {}; // 这样会创建一个 Map ,而不是 Set 。

是 Set 还是 Map ? Map 字面量语法同 Set 字面量语法非常相似。 因为先有的 Map 字面量先出现,所以 {} 默认是 Map 类型。 如果忘记在 {} 上注释类型或赋值到一个未声明类型的变量上, 那么 Dart 会创建一个类型为 Map<dynamic, dynamic> 的对象。

使用 add()addAll() 为已有的 Set 添加元素:

1
2
3
var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);

使用 .length 来获取 Set 中元素的个数:

1
2
3
4
var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);
assert(elements.length == 5);

在 Set 字面量前增加 const ,来创建一个编译时 Set 常量:

1
2
3
4
5
6
7
8
final constantSet = const {
  'fluorine',
  'chlorine',
  'bromine',
  'iodine',
  'astatine',
};
// constantSet.add('helium'); // Uncommenting this causes an error.

Set的一些常用API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var set = Set();
set.add(value);
set.addAll(elements);
set.remove(value);
set.removeAll(elements);
set.removeWhere((item)=>item>3);
set.contains(value);
set.containsAll(other);
set.isNotEmpty;
set.isEmpty;
set.length;
set.difference(other);//返回一个新集合,该集合的元素不在[other]中。
set.intersection(other);//返回一个新的集合,这个集合是这个集合和其他集合的交集。
set.union(other);//返回一个新的集合,其中包含这个集合和[其他]的所有元素。

Map

通常来说, Map 是用来关联 keys 和 values 的对象。 keys 和 values 可以是任何类型的对象。在一个 Map 对象中一个 key 只能出现一次。 但是 value 可以出现多次。 Dart 中 Map 通过 Map 字面量 和 Map 类型来实现。

下面是使用 Map 字面量的两个简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
var gifts = {
  // Key:    Value
  '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 中添加错误类型,那么分析器或者运行时会引发错误。

以上 Map 对象也可以使用 Map 构造函数创建:

1
2
3
4
5
6
7
8
9
var gifts = Map();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';

var nobleGases = Map();
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';

提示: 这里为什么只有 Map() ,而不是使用 new Map()。 因为在 Dart 中,new 关键字是可选的。

使用下标赋值运算符 []=( ) 向现有映射中添加新的键值对:

1
2
var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds'; // Add a key-value pair

使用下标运算符 ( ) 从映射中检索值[]

1
2
var gifts = {'first': 'partridge'};
assert(gifts['first'] == 'partridge');

如果 Map 中不包含所要查找的 key,那么 Map 返回 null:

1
2
var gifts = {'first': 'partridge'};
assert(gifts['fifth'] == null);

使用 .length 获取当前 Map 中的 key-value 对数量:

1
2
3
var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds';
assert(gifts.length == 2);

创建 Map 类型运行时常量,要在 Map 字面量前加上关键字 const。

1
2
3
4
5
6
7
final constantMap = const {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

// constantMap[2] = 'Helium'; // 取消注释会引起错误。

Map 常用API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var map = {'first': 'partridge'};
map['fourth'] = 'calling birds';
map.remove(key);
map.addAll(other);
map.addEntries(newEntries);
map.putIfAbsent(key, ifAbsent);//查找[key]的值,如果没有,则添加一个新值。
map.remove(key);
map.removeWhere((key,value)=>{});
map.clear();
map.containsKey(key);
map.containsValue(value);
map.length;
map.isNotEmpty;
map.isEmpty;


控制流运算符

Dart 提供了collection if and collection for 供列表、映射和集合字面​​量使用. 您可以使用通过条件(if)和重复 (for)这些运算符构建集合。 collection if

1
var nav = ['Home', 'Furniture', 'Plants', if (promoActive) 'Outlet'];

if-case

1
var nav = ['Home', 'Furniture', 'Plants', if (login case 'Manager') 'Inventory'];

collection for

1
2
3
var listOfInts = [1, 2, 3];
var listOfStrings = ['#0', for (var i in listOfInts) '#$i'];
assert(listOfStrings[1] == '#1');

记录 (Records)

需要至少 3.0 的语言版本

记录是一种匿名、不可变的聚合类型。与其他集合类型一样,它们允许将多个对象捆绑为一个对象。与其他集合类型不同,记录是固定大小、异构且类型化的。

记录是真实值;您可以将它们存储在变量中、嵌套它们、将它们传递到函数或从函数传递它们,以及将它们存储在列表、映射和集合等数据结构中。

记录语法

记录表达式 是用逗号分隔的命名或位置字段列表,括在括号中:

1
var record = ('first', a: 2, b: true, 'last');

记录类型注释 是用括号括起来的逗号分隔的类型列表。可以使用记录类型注释来定义返回类型和参数类型。例如,以下(int, int)语句是记录类型注释:

1
2
3
4
(int, int) swap((int, int) record) {
  var (a, b) = record;
  return (b, a);
}

记录表达式和类型注释中的字段反映了函数中参数和实参的工作方式。位置字段直接位于括号内:

1
2
3
4
5
// Record type annotation in a variable declaration:
(String, int) record;

// Initialize it with a record expression:
record = ('A string', 123);

在记录类型注释中,命名字段位于所有位置字段之后,位于类型和名称对的花括号分隔部分内。在记录表达式中,名称位于每个字段值之前,后面带有冒号:

1
2
3
4
5
// Record type annotation in a variable declaration:
({int a, bool b}) record;

// Initialize it with a record expression:
record = (a: 123, b: true);

记录类型中命名字段的名称是记录类型定义或其 形状 的一部分。两个具有不同名称的命名字段的记录具有不同的类型:

1
2
3
4
5
({int a, int b}) recordAB = (a: 1, b: 2);
({int x, int y}) recordXY = (x: 3, y: 4);

// Compile error! These records don't have the same type.
// recordAB = recordXY;

在记录类型注释中,您还可以命名 位置 字段,但这些名称纯粹用于文档,不会影响记录的类型:

1
2
3
4
(int a, int b) recordAB = (1, 2);
(int x, int y) recordXY = (3, 4);

recordAB = recordXY; // OK.

这类似于函数声明或函数 typedef 中的位置参数可以有名称,但这些名称不会影响函数的签名。

记录字段

记录字段可通过内置 getter 访问。记录是不可变的,因此字段没有 setter。 命名字段会公开同名的 getter。位置字段会公开名称为 的 getter $<position>,跳过命名字段:

1
2
3
4
5
6
var record = ('first', a: 2, b: true, 'last');

print(record.$1); // Prints 'first'
print(record.a); // Prints 2
print(record.b); // Prints true
print(record.$2); // Prints 'last'

记录类型

单个记录类型没有类型声明。记录根据其字段的类型进行结构化类型划分。记录的 形状(其字段集、字段类型及其名称(如果有))唯一地决定了记录的类型。

记录中的每个字段都有自己的类型。同一记录中的字段类型可能不同。无论从记录中访问哪个字段,类型系统都能识别每个字段的类型:

1
2
3
4
(num, Object) pair = (42, 'a');

var first = pair.$1; // Static type `num`, runtime type `int`.
var second = pair.$2; // Static type `Object`, runtime type `String`.

假设两个不相关的库创建了具有相同字段集的记录。即使库之间没有相互耦合,类型系统也会认为这些记录属于同一类型。

记录相等

如果两个记录具有相同的 形状 (字段集),并且其对应字段具有相同的值,则它们相等。由于命名字段 顺序 不是记录形状的一部分,因此命名字段的顺序不会影响相等性。

1
2
3
4
(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);

print(point == color); // Prints 'true'.
1
2
3
4
({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); // Prints 'false'. Lint: Equals on unrelated types.

记录会根据其字段的结构自动定义hashCode和方法。==

多次返回

记录允许函数返回捆绑在一起的多个值。要从返回中检索记录值, 使用模式匹配将值解构为局部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Returns multiple values in a record:
(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',
};

// Destructures using a record pattern with positional fields:
var (name, age) = userInfo(json);

/* Equivalent to:
  var info = userInfo(json);
  var name = info.$1;
  var age  = info.$2;
*/

您还可以使用冒号语法,通过其命名字段来解构记录

1
2
3
4
({String name, int age}) userInfo(Map<String, dynamic> json)
// ···
// Destructures using a record pattern with named fields:
final (:name, :age) = userInfo(json);

您可以从没有记录的函数返回多个值,但其他方法也有缺点。例如,创建一个类会更加冗长,而使用其他集合类型(如List或)Map会失去类型安全性。

泛型(Generics)

如果您查看基本数组类型的 API 文档,List您会发现该类型实际上是List<E>。 <…> 符号将 List 标记为  通用(或 参数化 )类型 - 具有正式类型参数的类型。按照惯例,大多数类型变量都有单字母名称,例如 E、T、S、K 和 V。

为什么要使用泛型?

泛型通常是类型安全所必需的,但它们的好处不仅仅是让你的代码运行:

  • 正确指定泛型类型可以生成更好的代码。
  • 您可以使用泛型来减少代码重复。

如果您希望列表仅包含字符串,则可以将其声明为List<String>(读作“字符串列表”)。这样,您、您的程序员同事和您的工具就可以检测到将非字符串分配给列表可能是一个错误。以下是一个例子:

1
2
3
4
 静态分析:失败
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Error

使用泛型的另一个原因是减少代码重复。泛型允许你在多种类型之间共享单个接口和实现,同时仍可利用静态分析。例如,假设你创建一个用于缓存对象的接口:

1
2
3
4
abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}

您发现您需要该接口的字符串特定版本,因此您创建了另一个接口:

1
2
3
4
abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}

稍后,您决定要使用这个界面的特定数字版本…您明白了。

泛型类型可以省去创建所有这些接口的麻烦。相反,你可以创建一个采用类型参数的接口:

1
2
3
4
abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}

T 是替代类型。它是一个占位符,您可以将其视为开发人员稍后将定义的类型。

使用集合字面量

列表、集合和映射字面量可以参数化。参数化字面量就像您已经见过的字面量一样,只是您在左括号前添加了<_type_>(对于列表和集合)或 <_keyType_, _valueType_>(对于映射)。以下是使用类型字面量的示例:

1
2
3
4
5
6
7
var names = <String>['Seth', 'Kathy', 'Lars'];
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
var pages = <String, String>{
  'index.html': 'Homepage',
  'robots.txt': 'Hints for web robots',
  'humans.txt': 'We are people, not machines'
};

将参数化类型与构造函数一起使用

要在使用构造函数时指定一个或多个类型,请将类型放在类名后面的尖括号 (<...> ) 中。例如:

1
var nameSet = Set<String>.from(names);

以下代码创建一个具有整数键和 View 类型值的映射:

1
var views = Map<int, View>();

泛型集合及其包含的类型

Dart 泛型类型是 具体化的,这意味着它们在运行时会携带其类型信息。例如,您可以测试集合的类型:

1
2
3
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true

相比之下,Java 中的泛型使用_擦除_,这意味着泛型类型参数在运行时被删除。在 Java 中,您可以测试一个对象是否是 List,但无法测试它是否是List<String>

限制参数化类型

实现泛型类型时,您可能希望限制可以作为参数提供的类型,以便参数必须是特定类型的子类型。您可以使用extends来实现这一点。

一个常见的用例是确保类型不可空,通过使其成为的子类型Object (而不是默认的Object?)。

1
2
3
class Foo<T extends Object> {
  // Any type provided to Foo for T must be non-nullable.
}

可以将extends与除了Object之外的其他类型一起使用。这是扩展 SomeBaseClass 的示例,以便可以在T类型的对象上调用SomeBaseClass的成员:

1
2
3
4
5
6
class Foo<T extends SomeBaseClass> {
  // Implementation goes here...
  String toString() => "Instance of 'Foo<$T>'";
}

class Extender extends SomeBaseClass {...}

可以使用SomeBaseClass或其任何子类型作为泛型参数:

1
2
var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();

不指定通用参数也是可以的:

1
2
var foo = Foo();
print(foo); // Instance of 'Foo<SomeBaseClass>'

指定任何非SomeBaseClass类型都会导致错误:

1
2
✗ 静态分析:失败
var foo = Foo<Object>();

使用泛型方法

方法和函数也允许类型参数:

1
2
3
4
5
6
T first<T>(List<T> ts) {
  // Do some initial work or error checking, then...
  T tmp = ts[0];
  // Do some additional checking or processing...
  return tmp;
}

这里first(T)上的泛型类型参数允许您在多个地方使用类型参数<T>

  • 在函数的返回类型中(T)。
  • 在参数类型中(List<T>)。
  • 在局部变量的类型中(T tmp)。

别名 (Typedefs)

类型别名(通常称为_typedef_ ,因为它是用关键字声明的typedef)是一种引用类型的简洁方式。以下是声明和使用名为IntList的类型别名的示例:

1
2
typedef IntList = List<int>;
IntList il = [1, 2, 3];

类型别名可以有类型参数:

1
2
3
typedef ListMapper<X> = Map<X, List<X>>;
Map<String, List<String>> m1 = {}; // Verbose.
ListMapper<String> m2 = {}; // Same thing but shorter and clearer.

在大多数情况下,我们建议使用内联函数类型,而不是函数的 typedef。但是,函数 typedef 仍然有用:

1
2
3
4
5
6
7
typedef Compare<T> = int Function(T a, T b);

int sort(int a, int b) => a - b;

void main() {
  assert(sort is Compare<int>); // True!
}

类型体系

Dart 是类型安全的编程语言:Dart 使用静态类型检查运行时检查的组合来确保变量的值始终与变量的静态类型或其他安全类型相匹配。尽管类型是必需的,但由于类型推断,类型的注释是可选的。 静态类型检查的一个好处是能够使用 Dart 的静态分析器在编译时找到错误。

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

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

1
2
3
4
5
6
7
8
9
 static analysis: failuredart
void printInts(List<int> a) => print(a);

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

上面的代码在调用 printInts(list) 时会在 list (高亮提示)上产生类型错误:

1
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> 静态类型。这是因为 list 变量的初始化声明 var list = [] 没有为分析器提供足够的信息来推断比 dynamic 更具体的类型参数。 printInts() 函数需要 List<int> 类型的参数,因此导致类型不匹配。

在创建 list 时添加类型注释 <int>(代码中高亮显示部分)后,分析器会提示无法将字符串参数分配给 int 参数。删除 list.add("2") 中的字符串引号使代码通过静态分析并能够正常执行。

1
2
3
4
5
6
7
8
9
 static analysis: successdart
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 是一个编译时错误。如果对象不是字符串,使用 as String 将对象转换为字符串时,会由于运行时错误而导致转换失败。

类型安全的好处

安全的类型系统有以下几个好处:

  • 在编译时就可以检查并显示类型相关的错误。
    安全的类型系统强制要求代码明确类型,因此在编译时会显示与类型相关的错误,这些错误可能在运行时可能很难发现。
  • 代码更容易阅读。
    代码更容易阅读,因为我们信赖一个拥有指定类型的值。在类型安全的 Dart 中,类型是不会骗人的。因为一个拥有指定类型的值是可以被信赖的。
  • 代码可维护性更高。
    在安全的类型系统下,当更改一处代码后,类型系统会警告因此影响到的其他代码块。
  • 更好的 AOT 编译。
    虽然在没有类型的情况下可以进行 AOT 编译,但生成的代码效率要低很多。

静态检查中的一些技巧

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

  • 重写方法时,使用类型安全返回值。
  • 重写方法时,使用类型安全的参数。
  • 不要将动态类型的 List 看做是有类型的 List。

让我们通过下面示例的类型结构,来更深入的了解这些规则: 类结构

重写方法时,使用类型安全的返回值

子类方法中返回值类型必须与父类方法中返回值类型的类型相同或其子类型。考虑 Animal 类中的 Getter 方法:

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

父类Getter 方法返回一个 Animal 。在 HoneyBadger 子类中,可以使用 HoneyBadger(或 Animal 的任何其他子类型)替换 Getter 的返回值类型,但不允许使用其他的无关类型。

1
2
3
4
5
6
7
8
 静态分析:成功
class HoneyBadger extends Animal {
  @override
  void chase(Animal a) { ... }

  @override
  HoneyBadger get parent => ...
}
1
2
3
4
5
6
7
8
 静态分析:失败
class HoneyBadger extends Animal {
  @override
  void chase(Animal a) { ... }

  @override
  Root get parent => ...
}

重写方法时,使用类型安全的参数。

子类方法的参数必须与父类方法中参数的类型相同或是其参数的父类型。不要使用原始参数的子类型,替换原有类型,这样会导致参数类型”收紧”。 考虑 Animal 的 chase(Animal) 方法:

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

chase() 方法的参数类型是 Animal 。一个 HoneyBadger 可以追逐任何东西。因此可以在重写 chase() 方法时将参数类型指定为任意类型 (Object) 。

1
2
3
4
5
6
7
8
 静态分析:成功
class HoneyBadger extends Animal {
  @override
  void chase(Object a) { ... }

  @override
  Animal get parent => ...
}

Mouse 是 Animal 的子类,下面的代码将 chase() 方法中参数的范围从 Animal 缩小到 Mouse 。

1
2
3
4
5
6
7
 静态分析:失败
class Mouse extends Animal { ... }

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

下面的代码不是类型安全的,因为 a 可以是一个 cat 对象,却可以给它传入一个 alligator 对象。

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

不要将动态类型的 List 看做是有类型的 List

当期望在一个 List 中可以包含不同类型的对象时,动态列表是很好的选择。但是不能将动态类型的 List 看做是有类型的 List 。

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

下面代码创建一个 Dog 的动态 List ,并将其分配给 Cat 类型的 List ,表达式在静态分析期间会产生错误。

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

运行时检查

运行时检查工具会处理分析器无法捕获的类型安全问题。

例如,以下代码在运行时会抛出异常,因为将 Dog 类型的 List 赋值给 Cat 类型的 List 是错误的:

1
2
3
4
5
 运行时:失败
void main() {
  List<Animal> animals = <Dog>[Dog()];
  List<Cat> cats = animals as List<Cat>;
}

类型推断

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

下面是在泛型中如何进行类型推断的示例。在此示例中,名为 arguments 的变量包含一个 Map ,该 Map 将字符串键与各种类型的值配对。

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

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

或者,使用 var 让 Dart 来推断类型:

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

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

字段和方法推断

重写父类且没有指定类型的字段或方法,继承父类中字段或方法的类型。

没有声明类型且不存在继承类型的字段,如果在声明时被初始化,那么字段的类型为初始化值的类型。

静态字段推断

静态字段和变量的类型从其初始化程序中推断获得。需要注意的是,如果推断是个循环,推断会失败。

局部变量推断

在不考虑连续赋值的情况下,局部变量如果有初始化值的情况下,其类型是从初始化值推断出来的。这可能意味着推断出来的类型会非常严格。如果是这样,可以为他们添加类型注释。

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

参数类型推断

构造函数调用的类型参数和泛型方法调用是根据上下文的向下信息和构造函数或泛型方法的参数的向上信息组合推断的。如果推断没有按照意愿或期望进行,那么你可以显式的指定他们的参数类型。

1
2
3
4
5
6
7
8
9
 静态分析:成功
// 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 。在推断 map() 方法的类型参数:<int> 时,Dart 使用此返回值的类型作为向上信息。

替换类型

当重写方法时,可以使用一个新类型(在新方法中)替换旧类型(在旧方法中)。类似地,当参数传递给函数时,可以使用另一种类型(实际参数)的对象替换现有类型(具有声明类型的参数)要求的对象。什么时候可以用具有子类型或父类型的对象替换具有一种类型的对象那?

消费者生产者 的角度有助于我们思考替换类型的情况。消费者接受类型,生产者产生类型。 可以使用父类型替换消费者类型,使用子类型替换生产者类型。 下面让我们看一下普通类型赋值和泛型类型赋值的示例。

普通类型赋值

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

分析以下类型层次结构:

类结构

思考下面示例中的普通赋值,其中 Cat c 是 消费者 而 Cat() 是 生产者

1
Cat c = Cat();

在消费者的位置,任意类型(Animal)的对象替换特定类型(Cat)的对象是安全的。因此使用 Animal c 替换 Cat c 是允许的,因为 Animal 是 Cat 的父类。

1
2
 静态分析:成功
Animal c = Cat();

但是使用 MaineCoon c 替换 Cat c 会打破类型的安全性,因为父类可能会提供一种具有不同行为的 Cat ,例如 Lion :

1
2
 静态分析:失败
MaineCoon c = Cat();

在生产者的位置,可以安全地将生产类型 (Cat) 替换成一个更具体的类型 (MaineCoon) 的对象。因此,下面的操作是允许的:

1
2
 静态分析:成功
Cat c = MaineCoon();

泛型赋值

上面的规则同样适用于泛型类型吗?是的。考虑动物列表的层次结构— Cat 类型的 List 是 Animal 类型 List 的子类型,是 MaineCoon 类型 List 的父类型。

类结构

在下面的示例中,可以将 MaineCoon 类型的 List 赋值给 myCats ,因为 List<MaineCoon> 是 List<Cat> 的子类型:

1
2
3
 静态分析:成功
List<MaineCoon> myMaineCoons = ...
List<Cat> myCats = myMaineCoons;

从另一个角度看,可以将 Animal 类型的 List 赋值给 List<Cat> 吗?

1
2
3
 静态分析:失败
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals;

这个赋值不能通过静态分析,因为它创建了一个隐式的向下转型 (downcast),这在非 dynamic 类型中是不允许的,比如 Animal

若要这段代码能够通过静态分析,需要使用一个显式转换,这可能会在运行时导致失败。

1
2
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals as List<Cat>;

不过,显式转换在运行时仍然可能会失败,这取决于转换被转换内容的实际类型 (此处是 myAnimals)。

方法

在重写方法中,生产者和消费者规则仍然适用。例如:

类结构

对于使用者(例如 chase(Animal) 方法),可以使用父类型替换参数类型。对于生产者(例如 父类 的 Getter 方法),可以使用子类型替换返回值类型。

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

关键字covariant

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

下面显示了如何使用covariant

1
2
3
4
5
6
7
8
9
10
11
 静态分析:成功
class Animal {
  void chase(Animal x) { ... }
}

class Mouse extends Animal { ... }

class Cat extends Animal {
  @override
  void chase(covariant Mouse x) { ... }
}
本文由作者按照 CC BY 4.0 进行授权