跳到主要内容

Built-in types(内置类型)

Dart 为数字、字符串、布尔值、集合、记录和函数等常用数据提供了内置类型与字面量语法。数字、字符串、集合和函数也都是对象,可以调用各自类型定义的成员;null 则是 Null 类型唯一的值。

学习 Dart 内置类型时,重点需要掌握以下问题:

  • intdoublenum 有什么关系?
  • ListSetMap 的字面量如何区分?
  • record 与类、列表有什么不同?
  • 函数值如何使用明确的函数类型?
  • Object?dynamicNevervoid 分别表达什么?

内置类型概览

下面这些类型在 Dart 代码中最常见:

数据类别Dart 类型字面量示例
整数int420x2A
浮点数double3.141.5e3
字符串String'Dart'"Dart"
布尔值booltruefalse
列表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 typeint Function(int, int)
Unicode 码点Runes'Dart'.runes
符号Symbol#build
空值Nullnull

TKV 是泛型类型参数,分别表示集合元素、映射键和映射值的类型。它们不是固定的类型名,使用集合时要替换成实际类型。

数字:intdoublenum

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 时,整数和浮点数字面量会分别推断为 intdouble

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 只有 truefalse 两个值。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.');
}
}

集合:ListSetMap

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

花括号同时用于 SetMap 字面量。空的 {} 默认创建 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(字素簇)能力,而不是依赖 lengthrunes.length

符号:Symbol

Symbol 表示 Dart 程序中声明的运算符或标识符名称。符号字面量以 # 开头:

void main() {
const symbol = #build;
print(symbol); // Symbol("build")
}

普通业务代码很少直接使用 Symbol。它主要出现在需要用稳定方式表示程序名称的 API 中,不应该用来代替普通字符串保存用户数据。

null、可空类型与特殊类型

nullNull 类型唯一的值。启用健全空安全后,不可空类型不能保存 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,
}
对比项DartTypeScript
数字区分 intdouble,共同父类型是 num通常统一为 number
布尔条件条件必须是 boolJavaScript 运行时允许真值与假值转换
record内置的结构化、浅层不可变值可用对象或 tuple 表达相近结构,但没有完全等价的 record 语义
空值使用 T? 表示可空类型通常使用 T | nullT | undefined
动态边界Object? 保留检查,dynamic 推迟检查unknown 保留检查,any 绕过许多检查

TypeScript 的对象类型可以帮助理解 Dart 的命名 record,但二者不是完全等价的。Dart record 的字段结构直接参与 record type,而需要封装行为的数据仍应使用类。

常见误区

误区一:把 var 当成一种内置类型

var 是声明变量并请求类型推断的关键字,不是类型。var value = 1 中,value 的静态类型是 int

误区二:认为所有数字都是同一种类型

intdouble 都属于 num,但它们仍是不同类型。需要同时接受两者时显式使用 num,只接受整数时使用 int

误区三:把 {} 当成空 Set

空的 {}Map<dynamic, dynamic>。空 Set<String> 应写成 <String>{}

误区四:使用 String.length 计算可见字符数

length 计算 UTF-16 编码单元。处理用户感知字符时,需要根据场景使用 characters 包,而不是假定一个可见字符的长度总是 1

误区五:认为 final 集合不可修改

final 限制变量不能重新赋值,不会自动冻结集合。需要编译时常量集合时使用 const;需要其他形式的不可变数据时,应选择能明确提供该约束的 API 或类型。

小结

  • intdouble 都是 num 的子类型,但二者是不同的静态类型。
  • String 是 UTF-16 编码单元序列,字符串长度不一定等于用户感知字符数。
  • Dart 的条件必须是 bool,不存在 JavaScript 风格的隐式真值转换。
  • List 有顺序,Set 去重,Map 保存键值对;空 {} 默认是 Map
  • record 用于组合固定结构的数据,函数值应尽量使用明确的函数签名。
  • null 只能赋给可空类型;Object?dynamicNevervoid 表达不同的类型边界。
  • var 负责类型推断,不是内置类型,也不会让变量变成动态类型。