Dart 简介
变量
- 用 var 来定义变量,而不用显式指定它们的类型。由于其支持类型推断,因此大多数变量的类型会由它们的初始化内容决定:
var name = 'Voyager I';
var year = 1977;
var antennaDiameter = 3.7;
var flybyObjects = ['Jupiter', 'Saturn', 'Uranus', 'Neptune'];
var image = {
'tags': ['saturn'],
'url': '//path/to/saturn.jpg',
};
- 延迟初始化变量: 如果你确定变量在使用之前已设置,但 Dart 推断错误的话,可以将变量标记为 late 来解决这个问题:
late String description;
void main() {
description = 'Feijoada!';
print(description);
}
- 如果你不打算更改一个变量,可以使用 final 或 const 修饰它
final name = 'Bob'; // Without a type annotation
final String nickname = 'Bobby';
const bar = 1000000; // Unit of pressure (dynes/cm2)
const double atm = 1.01325 * bar; // Standard atmosphere
导入 (Import)
// Importing core libraries
import 'dart:math';
// Importing libraries from external packages
import 'package:test/test.dart';
// Importing files
import 'path/to/my_other_file.dart';
枚举类型 (Enum)
枚举(enum)支持定义 方法、字段 和 构造函数,可以看做特殊的类,推荐使用。
- 下面这个简单的枚举示例定义了一组行星类别:
enum PlanetType { terrestrial, gas, ice }
- 下面是一个增强型枚举的示例,定义了一组行星类的常量实例,即太阳系的行星:
/// Enum that enumerates the different planets in our solar system
/// and some of their properties.
enum Planet {
mercury(planetType: PlanetType.terrestrial, moons: 0, hasRings: false),
venus(planetType: PlanetType.terrestrial, moons: 0, hasRings: false),
// ···
uranus(planetType: PlanetType.ice, moons: 27, hasRings: true),
neptune(planetType: PlanetType.ice, moons: 14, hasRings: true);
/// A constant generating constructor
const Planet({
required this.planetType,
required this.moons,
required this.hasRings,
});
/// All instance variables are final
final PlanetType planetType;
final int moons;
final bool hasRings;
/// Enhanced enums support getters and other methods
bool get isGiant =>
planetType == PlanetType.gas || planetType == PlanetType.ice;
}
- 你可以这样使用 Planet 枚举:
final yourPlanet = Planet.earth;
if (!yourPlanet.isGiant) {
print('Your planet is not a "giant planet".');
}
扩展类(继承)
Dart 支持单继承。
class Orbiter extends Spacecraft {
double altitude;
Orbiter(super.name, DateTime super.launchDate, this.altitude);
}
Mixins
- Mixin 是一种在多个类层次结构中重用代码的方法。
mixin Piloted {
int astronauts = 1;
void describeCrew() {
print('Number of astronauts: $astronauts');
}
}
- 现在你只需使用 Mixin 的方式继承这个类就可将该类中的功能添加给其它类。
class PilotedCraft extends Spacecraft with Piloted {
// ···
}
接口和抽象类
- 所有的类都隐式定义成了一个接口。因此,任意类都可以作为接口被实现。
class MockSpaceship implements Spacecraft {
// ···
}
- 你可以创建一个被任意具体类扩展(或实现)的抽象类。抽象类可以包含抽象方法(不含方法体的方法)。
abstract class Describable {
void describe();
void describeWithEmphasis() {
print('=========');
describe();
print('=========');
}
}
异步
- 使用
async
和await
关键字可以让你避免回调地狱 (Callback Hell) 并使你的代码更具可读性。
const oneSecond = Duration(seconds: 1);
// ···
Future<void> printWithDelay(String message) async {
await Future.delayed(oneSecond);
print(message);
}
运算符Operators
- 基本的加减乘除和其他语言一样,取商和取余操作带来方便。
assert(5 ~/ 2 == 2); // 结果是整型; 取商
assert(5 % 2 == 1); // 余数
- 自增自减,关系运算符,赋值运算,逻辑运算,位运算等都和C++差不多。
类型判定运算符
- 类型检查:is和is!
if (emp is Person) {
// Type check
emp.firstName = 'Bob';
}
- 使用as进行类型转换,类型不匹配会抛出异常,可以考虑用try catch结构,但是会影响性能,应尽量少用
void main() {
Object? obj = 42;
try {
String str = obj as String; // 可能抛出CastError
print(str.length);
} catch (e) {
print('类型转换失败: $e');
}
}
- 使用条件类型转换(is + 类型提升),替代as
void main() {
Object? obj = 42; // 假设这是一个未知类型的对象
if (obj is String) {
// 类型提升:Dart自动将obj视为String类型
print(obj.length); // 安全访问String属性
} else {
print('obj不是String类型');
}
}
T? safeCast<T>(dynamic value) {
if (value is T) return value;
return null;
}
void main() {
Object? obj = 42;
String? str = safeCast<String>(obj);
print(str); // 输出:null
}
- 对可空类型使用as? 如果类型不匹配,返回null而不是抛出异常。(这个应该作用as的主要使用方式)
void main() {
Object? obj = 42;
// 如果obj不是String类型,str为null
String? str = obj as? String;
print(str); // 输出:null
// 安全访问属性
print(str?.length); // 输出:null
}
- 集合操作使用whereType<T>():过滤并转换符合条件的元素。避免使用as
void main() {
List<Object?> mixedList = ['a', 1, null, true];
// 只保留String类型的元素
List<String> strings = mixedList.whereType<String>().toList();
print(strings); // 输出:[a]
}
条件表达式
- 如果赋值是根据布尔值, 考虑使用 ?:。
var visibility = isPublic ? 'public' : 'private';
- 如果赋值是基于判定是否为 null, 考虑使用 ??。
String playerName(String name) => name ?? 'Guest';
级联运算符 (..)
能看懂就行,平时还是不要用了,鸡肋功能,意义不大
querySelector('#confirm') // 获取对象。
..text = 'Confirm' // 调用成员变量。
..classes.add('important')
..onClick.listen((e) => window.alert('Confirmed!'));
上面的代码等价于:
var button = querySelector('#confirm');
button.text = 'Confirm';
button.classes.add('important');
button.onClick.listen((e) => window.alert('Confirmed!'));
内建类型Built-in types
数值类型的分类
- num:抽象基类,所有数值类型的父类。
- int:整数类型,范围为 -2⁶³ 到 2⁶³-1。
- double:双精度浮点数,符合 IEEE 754 标准。
void main() {
// 整数
int age = 25;
int maxValue = 2147483647; // 2³¹-1(在32位系统上)
// 浮点数
double pi = 3.14159;
double price = 9.99;
// num类型(可接受int或double)
num count = 10; // int类型
num temperature = 25.5; // double类型
// 科学计数法
double largeNumber = 1.23e5; // 1.23 × 10⁵ = 123000
}
对应于后台Java的number类型,客户端用num进行对接是最合适,避免因为类型不匹配导致异常。
数值格式化
- 使用NumberFormat类(需导入intl包):
import 'package:intl/intl.dart';
void main() {
double price = 1234.5678;
// 货币格式化
String formattedPrice = NumberFormat.currency(symbol: '¥', decimalDigits: 2).format(price);
print(formattedPrice); // ¥1,234.57
// 自定义格式
String customFormat = NumberFormat('#,##0.00').format(price);
print(customFormat); // 1,234.57
}
对于货币,日期等特殊格式,可以考虑用这个。
精度问题
- 在 Dart 中,double.toString() 方法可能会因为浮点数精度问题导致输出过长的字符串。这是 IEEE 754 双精度浮点数格式的固有特性,不仅限于 Dart,几乎所有编程语言都会遇到这个问题。
void main() {
double d = 0.1;
print(d.toString()); // 输出:0.100000000000000055511151231257827021181583404541015625
}
- 使用 toStringAsFixed() 控制小数位数,将浮点数格式化为指定小数位数的字符串,自动四舍五入。(这是显示数字最常用的方法)
void main() {
double d = 0.1;
print(d.toStringAsFixed(2)); // 输出:0.10
}
String
- 使用连续三个单引号或者三个双引号实现多行字符串对象的创建:
var s1 = '''
You can create
multi-line strings like this one.
''';
var s2 = """This is also a
multi-line string.""";
- 使用 r 前缀,可以创建 “原始 raw” 字符串:
var s = r"In a raw string, even \n isn't special.";
Boolean
Dart 使用 bool 类型表示布尔值。 Dart 只有字面量 true and false 是布尔类型, 这两个对象都是编译时常量。
Dart 的类型安全意味着不能使用 if (nonbooleanValue) 或者 assert (nonbooleanValue)。 而是应该像下面这样,明确的进行值检查:
// 检查空字符串。
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);
List
几乎每种编程语言中最常见的集合可能是 array 或有序的对象集合。 在 Dart 中的 Array 就是 List对象
Lists 的下标索引从 0 开始,第一个元素的索引是 0。 list.length - 1 是最后一个元素的索引。
var list = [1, 2, 3];
assert(list.length == 3);
assert(list[1] == 2);
list[1] = 1;
assert(list[1] == 1);
Set
- 在 Dart 中 Set 是一个元素唯一且无序的集合。下面是通过字面量创建 Set 的一个简单示例:
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
- 要创建一个空集,使用前面带有类型参数的 {} ,或者将 {} 赋值给 Set 类型的变量:
var names = <String>{};
// Set<String> names = {}; // 这样也是可以的。
// var names = {}; // 这样会创建一个 Map ,而不是 Set 。
Map
- 通常来说, Map 是用来关联 keys 和 values 的对象。 keys 和 values 可以是任何类型的对象。在一个 Map 对象中一个 key 只能出现一次。 但是 value 可以出现多次。
var gifts = {
// Key: Value
'first': 'partridge',
'second': 'turtledoves',
'fifth': 'golden rings'
};
var nobleGases = {
2: 'helium',
10: 'neon',
18: 'argon',
};
- 如果 Map 中不包含所要查找的 key,那么 Map 返回 null:
var gifts = {'first': 'partridge'};
assert(gifts['fifth'] == null);
Rune
在 Dart 中, Rune 用来表示字符串中的 UTF-32 编码字符。
Unicode 码点:每个 Unicode 字符对应一个唯一的数字(如 A → U+0041,😀 → U+1F600)。
UTF-16 编码:Dart 字符串使用 UTF-16 编码,基本字符(如 ASCII)用 1 个代码单元(16 位)表示,复杂字符(如表情符号)用 2 个代码单元(32 位)表示。
rune:本质是一个整数,代表 Unicode 码点的值(如 0x1F600 表示 😀)。
字符串与 rune 互转
void main() {
// 字符串 → rune
String emoji = '😀';
List<int> runes = emoji.runes.toList();
print(runes); // [128512](十进制的0x1F600)
// rune → 字符串
String fromRune = String.fromCharCode(0x1F600); // 或 String.fromCharCode(128512)
print(fromRune); // 😀
// 多个rune → 字符串
String multipleRunes = String.fromCharCodes([0x1F600, 0x1F601]);
print(multipleRunes); // 😀😁
}
- 遍历字符串中的 rune
void main() {
String str = 'Hello 😀 你好';
// 方式1:使用runes属性
str.runes.forEach((rune) {
print(String.fromCharCode(rune)); // 正确输出每个字符
});
// 方式2:使用for-in循环(自动处理rune)
for (var char in str) {
print(char); // 自动处理复杂字符
}
}
- 处理包含复杂字符的字符串长度
void main() {
String str = 'A😀B';
// 错误:基于UTF-16代码单元的长度
print(str.length); // 4(A占1个,😀占2个,B占1个)
// 正确:基于rune的长度
print(str.runes.length); // 3(A、😀、B各占1个rune)
}
- 比较和排序 rune
void main() {
String str1 = 'A';
String str2 = 'B';
// 比较Unicode码点
int comparison = str1.runes.first.compareTo(str2.runes.first);
print(comparison); // -1(A的码点小于B)
// 排序字符串(按Unicode码点)
List<String> chars = ['B', 'A', '😀'];
chars.sort((a, b) => a.runes.first.compareTo(b.runes.first));
print(chars); // [A, B, 😀]
}
- 复杂场景可使用第三方库(如 characters 包)简化操作:
import 'package:characters/characters.dart';
void main() {
String str = 'A😀B';
int length = str.characters.length; // 3(正确)
}
Symbol
在 Dart 中,Symbol 是一种特殊的对象,用于在运行时表示标识符(如类名、方法名、变量名)。它主要用于反射(reflection)、元编程(metaprogramming)和代码生成工具。
创建 Symbol
void main() {
// 方式1:使用#语法
Symbol methodSymbol = #myMethod;
Symbol fieldSymbol = #name;
// 方式2:使用Symbol构造函数(不推荐,性能较差)
Symbol dynamicSymbol = Symbol('runtimeName');
print(methodSymbol); // Symbol("myMethod")
}
- 反射(dart:mirrors)仅在 JIT 模式下可用(如开发环境),AOT 编译(如生产环境)会移除相关代码。
import 'dart:mirrors';
class Person {
String name = 'Alice';
void greet() {
print('Hello!');
}
}
void main() {
Person person = Person();
InstanceMirror mirror = reflect(person);
// 通过Symbol访问字段
Symbol nameSymbol = #name;
dynamic name = mirror.getField(nameSymbol).reflectee;
print(name); // Alice
// 通过Symbol调用方法
Symbol greetSymbol = #greet;
mirror.invoke(greetSymbol, []); // 输出:Hello!
}
- 将常用的 Symbol 定义为常量,避免重复创建:
const Symbol nameSymbol = #name;
const Symbol ageSymbol = #age;
- 在 json_serializable 等代码生成工具中,使用 Symbol 定义 JSON 字段名:
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
@JsonKey(name: #full_name) // 等价于 name: 'full_name'
String fullName;
User(this.fullName);
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
- 自定义元数据
class MyAnnotation {
final Symbol value;
const MyAnnotation(this.value);
}
@MyAnnotation(#myField)
class MyClass {
// ...
}
void main() {
// 通过反射获取注解(需要dart:mirrors)
ClassMirror mirror = reflectClass(MyClass);
InstanceMirror annotation = mirror.metadata.first;
Symbol symbolValue = annotation.reflectee.value;
print(symbolValue); // Symbol("myField")
}
函数基础
函数是一等对象
- 一个函数可以作为另一个函数的参数
void printElement(int element) {
print(element);
}
var list = [1, 2, 3];
// 将 printElement 函数作为参数传递。
list.forEach(printElement);
- 同样可以将一个函数赋值给一个变量
var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');
匿名函数
可以创建没有名字的函数,这种函数被称为 匿名函数, 有时候也被称为 lambda 或者 closure 。 匿名函数可以赋值到一个变量中
void main() {
var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
print('${list.indexOf(item)}: $item');
});
}
词法闭包
- 闭包 即一个函数对象,即使函数对象的调用在它原始作用域之外, 依然能够访问在它词法作用域内的变量。
- 函数可以封闭定义到它作用域内的变量。 接下来的示例中, makeAdder() 捕获了变量 addBy。 无论在什么时候执行返回函数,函数都会使用捕获的 addBy 变量。
/// 返回一个函数,返回的函数参数与 [addBy] 相加。
Function makeAdder(num addBy) {
return (num i) => addBy + i;
}
void main() {
// 创建一个加 2 的函数。
var add2 = makeAdder(2);
// 创建一个加 4 的函数。
var add4 = makeAdder(4);
assert(add2(3) == 5);
assert(add4(3) == 7);
}
返回值
所有函数都会返回一个值。 如果没有明确指定返回值, 函数体会被隐式的添加 return null; 语句。
foo() {}
assert(foo() == null);
命名可选参数
定义函数时,使用 {param1, param2, …} 来指定命名参数:
/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold, bool hidden}) {...}
位置可选参数
- 将参数放到 [] 中来标记参数是可选的:
String say(String from, String msg, [String device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}
- 下面是不使用可选参数调用上面方法 的示例:
assert(say('Bob', 'Howdy') == 'Bob says Howdy');
- 下面是使用可选参数调用上面方法的示例:
assert(say('Bob', 'Howdy', 'smoke signal') ==
'Bob says Howdy with a smoke signal');
默认参数值
在定义方法的时候,可以使用 = 来定义可选参数的默认值。 默认值只能是编译时常量。 如果没有提供默认值,则默认值为 null。
/// 设置 [bold] 和 [hidden] 标志 ...
void enableFlags({bool bold = false, bool hidden = false}) {...}
// bold 值为 true; hidden 值为 false.
enableFlags(bold: true);