Built-in types(内置类型)
Dart 为数字、字符串、布尔值、集合、记录和函数等常用数据提供了内置类型与字面量语法。数字、字符串、集合和函数也都是对象,可以调用各自类型定义的成员;null 则是 Null 类型唯一的值。
学习 Dart 内置类型时,重点需要掌握以下问题:
int、double和num有什么关系?List、Set和Map的字面量如何区分?- record 与类、列表有什么不同?
- 函数值如何使用明确的函数类型?
Object?、dynamic、Never和void分别表达什么?
内置类型概览
下面这些类型在 Dart 代码中最常见:
| 数据类别 | Dart 类型 | 字面量示例 |
|---|---|---|
| 整数 | int | 42、0x2A |
| 浮点数 | double | 3.14、1.5e3 |
| 字符串 | String | 'Dart'、"Dart" |
| 布尔值 | bool | true、false |
| 列表 | List<T> | <int>[1, 2, 3] |
| 集合 | Set<T> | <int>{1, 2, 3} |
| 映射 | Map<K, V> | <String, int>{'Dart': 3} |
| 记录 | record type | (name: 'Dart', version: 3) |
| 函数 | function type | int Function(int, int) |
| Unicode 码点 | Runes | 'Dart'.runes |
| 符号 | Symbol | #build |
| 空值 | Null | null |
T、K 和 V 是泛型类型参数,分别表示集合元素、映射键和映射值的类型。它们不是固定的类型名,使用集合时要替换成实际类型。
数字:int、double 和 num
Dart 使用 int 表示整数,使用 double 表示双精度浮点数。二者都是 num 的子类型,因此 num 变量既可以保存整数,也可以保存浮点数。
void main() {
int completedLessons = 12;
double progress = 0.75;
num score = 90;
score = 95.5;
print(completedLessons); // 12
print(progress); // 0.75
print(score); // 95.5
}
使用 var 时,整数和浮点数字面量会分别推断为 int 和 double:
void main() {
var count = 10; // int
var ratio = 0.5; // double
// count = 10.5; // 编译错误:double 不能赋值给 int
print('$count, $ratio');
}
常用的数字操作包括:
void main() {
print(7 / 2); // 3.5,结果为 double
print(7 ~/ 2); // 3,向零方向截断后的 int
print(int.parse('42')); // 42
print(double.parse('3.14')); // 3.14
}
浮点数采用二进制表示,某些十进制小数无法被精确保存。因此,不要直接用浮点数处理必须精确到最小单位的金额;可以根据业务要求改用整数保存最小单位,或选用合适的十进制定点方案。
字符串:String
String 表示 UTF-16 编码单元序列。单引号和双引号都能创建字符串,选择其中一种并保持风格一致即可。
void main() {
const language = 'Dart';
const greeting = "Hello";
print('$greeting, $language!');
print('${language.toUpperCase()} has ${language.length} letters.');
}
$variable 可以插入变量,${expression} 可以插入表达式。相邻的字符串字面量会自动连接:
const message = 'Built-in '
'types';
三引号用于书写多行字符串,前缀 r 用于创建原始字符串。原始字符串不会处理反斜杠转义和 $ 插值:
void main() {
const description = '''first line
second line''';
const windowsPath = r'C:\learning\dart';
print(description);
print(windowsPath);
}
布尔值:bool
bool 只有 true 和 false 两个值。Dart 的条件表达式必须是布尔值,不会把 0、空字符串、空集合或 null 自动转换为 false。
void main() {
const topics = <String>[];
final hasTopics = topics.isNotEmpty;
if (hasTopics) {
print('Continue learning');
} else {
print('Choose a topic');
}
}
下面的写法无法通过静态检查:
void main() {
const count = 1;
// if (count) {} // 编译错误:条件必须是 bool
if (count > 0) {
print('The count is positive.');
}
}
集合:List、Set 和 Map
Dart 内置三种常用集合类型:
List<T>:有顺序、允许重复元素,并通过从0开始的索引访问。Set<T>:不保存重复元素,适合判断某个值是否存在。Map<K, V>:保存键值对,每个键最多对应一个值。
void main() {
final topics = <String>['types', 'collections', 'functions'];
final tags = <String>{'dart', 'types', 'dart'};
final scores = <String, int>{
'variables': 90,
'types': 95,
};
print(topics[0]); // types
print(tags); // {dart, types}
print(scores['types']); // 95
}
List 保留重复元素,Set 会按照相等性去除重复元素。读取不存在的 Map 键会得到 null,所以上例中 scores['types'] 的静态类型是 int?,不是 int。
区分空 Set 和空 Map
花括号同时用于 Set 和 Map 字面量。空的 {} 默认创建 Map<dynamic, dynamic>,创建空 Set 时需要写出元素类型:
void main() {
var emptyMap = {}; // Map<dynamic, dynamic>
var emptySet = <String>{}; // Set<String>
emptyMap['language'] = 'Dart';
emptySet.add('types');
print(emptyMap);
print(emptySet);
}
集合变量的类型与集合本身是否可修改是两回事。final 只禁止变量重新指向另一个集合,仍可以修改原集合;const 集合则不能添加、删除或替换元素。
void main() {
final editable = <String>['types'];
editable.add('records');
const fixed = <String>['types'];
// fixed.add('records'); // 运行时会抛出不支持修改的错误
print(editable);
print(fixed);
}
记录:record
record 用于把固定数量、不同类型的值组合成一个轻量数据对象。record 没有需要预先声明的类;它的类型由字段的结构决定。
void main() {
var course = (title: 'Dart types', lessons: 8);
print(course.title); // Dart types
print(course.lessons); // 8
var (title: title, lessons: lessons) = course;
print('$title: $lessons lessons');
}
record 可以有位置字段、命名字段,或同时包含两者:
void main() {
(String, int) topic = ('Built-in types', 1);
({int x, int y}) point = (x: 10, y: 20);
print(topic.$1); // Built-in types
print(topic.$2); // 1
print(point.x + point.y); // 30
}
record 是浅层不可变的:字段不能重新赋值,但字段引用的对象本身仍可能可变。record 适合临时组合或返回多个值;当数据需要方法、校验、封装或稳定的领域名称时,通常应声明类。
版本说明:record 和 record patterns 需要 Dart 3.0 或更高语言版本。
函数类型
函数在 Dart 中是一等对象,可以赋给变量、作为参数传递,也可以作为返回值。函数变量应尽量保留明确的参数与返回值类型。
int add(int left, int right) => left + right;
int calculate(
int left,
int right,
int Function(int, int) operation,
) {
return operation(left, right);
}
void main() {
int Function(int, int) operation = add;
print(operation(2, 3)); // 5
print(calculate(4, 5, add)); // 9
}
虽然宽泛的 Function 类型可以表示函数对象,但它没有说明调用所需的参数和返回值。已知函数签名时,优先使用 返回类型 Function(参数类型) 这样的具体函数类型。
Unicode 码点与 Runes
String.length 返回 UTF-16 编码单元数量,不一定等于 Unicode 码点数量,也不一定等于用户看到的字符数量。String.runes 可以按 Unicode 码点读取字符串。
void main() {
const face = '\u{1F600}';
print(face.length); // 2 个 UTF-16 编码单元
print(face.runes.length); // 1 个 Unicode 码点
print(face.runes.first.toRadixString(16)); // 1f600
}
一个用户感知字符还可能由多个码点组成,例如带组合符号的文字或由多个符号组成的表情。需要按用户看到的完整字符切分文本时,应使用 characters 包提供的 grapheme cluster(字素簇)能力,而不是依赖 length 或 runes.length。
符号:Symbol
Symbol 表示 Dart 程序中声明的运算符或标识符名称。符号字面量以 # 开头:
void main() {
const symbol = #build;
print(symbol); // Symbol("build")
}
普通业务代码很少直接使用 Symbol。它主要出现在需要用稳定方式表示程序名称的 API 中,不应该用来代替普通字符串保存用户数据。
null、可空类型与特殊类型
null 是 Null 类型唯一的值。启用健全空安全后,不可空类型不能保存 null;在类型后添加 ? 才能表示值可以为空。
void main() {
String title = 'Built-in types';
String? subtitle;
// title = null; // 编译错误:String 不接受 null
subtitle = null;
print(title);
print(subtitle); // null
}
除了前面的数据类型,理解 Dart 类型系统时还会遇到这些特殊类型:
| 类型 | 作用 |
|---|---|
Object | 可以引用任意非空对象,但只能直接使用 Object 已知的成员 |
Object? | 可以引用任意对象或 null,同时保留静态类型检查 |
dynamic | 允许动态成员访问,把许多检查推迟到运行时 |
Never | 表示表达式不可能正常产生值,例如总是抛出异常的函数 |
void | 主要表示调用者不使用函数的返回值 |
Never fail(String message) {
throw StateError(message);
}
void logTopic(String topic) {
print(topic);
}
Never 没有任何值,因此返回 Never 的函数不能正常返回。void 则不表示函数永远不会返回;logTopic() 会正常执行结束,只是调用者不应使用它的返回值。
Object? 与 dynamic 的详细选择方式见 Variables(变量)。
与 TypeScript 的关键差异
Dart 和 TypeScript 都提供类型推断、集合与函数类型,但内置类型的划分并不完全相同。
int count = 1;
double ratio = 0.5;
List<String> topics = ['types'];
({String name, int lessons}) course = (name: 'Dart', lessons: 8);
let count: number = 1
let ratio: number = 0.5
let topics: string[] = ['types']
let course: { name: string; lessons: number } = {
name: 'Dart',
lessons: 8,
}
| 对比项 | Dart | TypeScript |
|---|---|---|
| 数字 | 区分 int 和 double,共同父类型是 num | 通常统一为 number |
| 布尔条件 | 条件必须是 bool | JavaScript 运行时允许真值与假值转换 |
| record | 内置的结构化、浅层不可变值 | 可用对象或 tuple 表达相近结构,但没有完全等价的 record 语义 |
| 空值 | 使用 T? 表示可空类型 | 通常使用 T | null 或 T | undefined |
| 动态边界 | Object? 保留检查,dynamic 推迟检查 | unknown 保留检查,any 绕过许多检查 |
TypeScript 的对象类型可以帮助理解 Dart 的命名 record,但二者不是完全等价的。Dart record 的字段结构直接参与 record type,而需要封装行为的数据仍应使用类。
常见误区
误区一:把 var 当成一种内置类型
var 是声明变量并请求类型推断的关键字,不是类型。var value = 1 中,value 的静态类型是 int。
误区二:认为所有数字都是同一种类型
int 和 double 都属于 num,但它们仍是不同类型。需要同时接受两者时显式使用 num,只接受整数时使用 int。
误区三:把 {} 当成空 Set
空的 {} 是 Map<dynamic, dynamic>。空 Set<String> 应写成 <String>{}。
误区四:使用 String.length 计算可见字符数
length 计算 UTF-16 编码单元。处理用户感知字符时,需要根据场景使用 characters 包,而不是假定一个可见字符的长度总是 1。
误区五:认为 final 集合不可修改
final 限制变量不能重新赋值,不会自动冻结集合。需要编译时常量集合时使用 const;需要其他形式的不可变数据时,应选择能明确提供该约束的 API 或类型。
小结
int和double都是num的子类型,但二者是不同的静态类型。String是 UTF-16 编码单元序列,字符串长度不一定等于用户感知字符数。- Dart 的条件必须是
bool,不存在 JavaScript 风格的隐式真值转换。 List有顺序,Set去重,Map保存键值对;空{}默认是Map。- record 用于组合固定结构的数据,函数值应尽量使用明确的函数签名。
null只能赋给可空类型;Object?、dynamic、Never和void表达不同的类型边界。var负责类型推断,不是内置类型,也不会让变量变成动态类型。