Dart 变量声明

在Dart中变量声明的关键词是var或者直接标明变量类型,例如:

1
2
var name = 'Bob';
String name = 'Bob';

而final和const主要是针对常量的命名,差别主要体现在不同的赋值时机:

  • final是在运行时进行赋值
  • const则是编译时进行赋值

比如在实际开发过程中,基于一个widget举例,我们可以使用final去修饰这个widget的一些属性,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyWidget extends StatelessWidget {
final String title;
final Color color;

MyWidget({this.title, this.color});

@override
Widget build(BuildContext context) {
return Container(
color: color,
child: Text(title),
);
}
}

当我们在使用这个widget的时候,就可以给这个widget传递title和color的值,这样就可以保证这个widget的属性是不可变的。

而const只能放在顶层或者静态变量中,比如:

1
2
3
const a = 1;
const b = 2;
const c = a + b;

这样的代码是可以通过编译的,因为const是在编译时进行赋值的,所以这样的代码是可以通过编译的。
那么反面案例则是:

1
const d = DateTime.now();

d的取值只有在运行时才能具体得知,所以d是无法通过编译的。
除此之外,在class内部也可以使用const去修饰一些属性,比如:

1
2
3
class MyWidget extends StatelessWidget {
static const String title = 'MyWidget';
}

比如上述这样的静态属性就可以使用const来修饰。

在类的构造函数中,也可以使用const去修饰构造函数,比如:

1
2
3
4
5
6
7
8
9
10
11
12
class Point {
final double x;
final double y;

const Point(this.x, this.y);
}

const p1 = Point(0, 0);
const p2 = Point(0, 0);

// p1 和 p2 实际上是同一个实例
print(identical(p1, p2)); // true

这样的构造函数就是一个常量的构造函数,在通过这个构造函数去创建多个实例的时候,多个实例占据的实际上是同一段内存空间,这样可以减少内存的占用。

此处我还需要补充一个很小的点,在开发flutter页面时,也可以用const去修饰不可变的组件,比如:

1
const Text("Text");

这样可以减少不必要的widget重建,提高性能。

类型推导与显式声明

在Dart中,即使不标明变量类型,Dart也可以根据变量的值来推导出变量的类型,比如:

1
var name = 'Bob';

这与JavaScript中的let和const有点类似,但是Dart的类型推导更加严格,比如:

1
2
var name = 'Bob';
name = 1; // 这里会报错,因为name已经被推导为String类型

而对比到JavaScript中,这样的代码是可以正常运行的。

此外,Dart还有一个特殊的类型dynamic,这个类型是在运行时才能确定的,比如:

1
2
dynamic name = 'Bob';
name = 1; // 这里不会报错,因为name是dynamic类型

但是我的开发习惯是尽量地避免使用dynamic类型,因为这样会导致在项目逐渐变得庞大后,变量的类型难以确定。

空安全

Dart中的空安全是指在编译时就能确定变量是否为空,这样可以避兼容性问题,比如:

1
String name = null; // 这里会报错,因为name是String类型,不能为null

在Dart中,?和!操作符是用来处理空安全的,?表示这个变量可以为空,!表示这个变量不为空,比如:

1
2
3
4
5
6
String? name = null;
print(name?.length); // 这里不会报错,因为name可能为空
print(name!.length); // 这里会报错,因为name可能为空

String name = '1';
print(name!.length); // 这里不会报错,因为name不为空

这样的空安全机制可以在编译时就能发现潜在的问题,提高代码的健壮性。

异步编程

Dart的异步编程主要是通过Future和Stream来实现的,这里先着重说一下Future。
Future是一个和JavaScript的Promise很类似的概念,它表示一个异步操作的结果,比如:

1
2
3
Future<String> fetchUserOrder() {
return Future.delayed(Duration(seconds: 2), () => 'Large Latte');
}
1
2
3
4
5
6
7
function fetchUserOrder(): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Large Latte');
}, 2000);
});
}

这两段代码是非常类似的,都是表示一个异步操作的结果,只不过Dart中的Future是一个类,而JavaScript中的Promise是一个构造函数。可以说,
只要明白了Promise的用法,那么Future也就不难理解了。

async, await, Future.delayed

Dart中的async和await与JavaScript中的async和await是非常类似的,都是用来处理异步操作的,比如:

1
2
3
4
5
Future<void> printOrderMessage() async {
var order = await fetchUserOrder();
print('Awaiting user order...');
print('Your order is: $order');
}
1
2
3
4
5
async function printOrderMessage() {
var order = await fetchUserOrder();
console.log('Awaiting user order...');
console.log('Your order is: ', order);
}

整体的写法也是大同小异的,这里再额外补充一个无栈协程的概念,是我看到的一篇v2ex上的文章,很不错,可以参考一下。

Future.delayed是一个延迟执行的方法,和JavaScript中的setTimeout是类似的,比如:

1
2
3
Future.delayed(Duration(seconds: 2), () {
print('2 seconds later');
});
1
2
3
setTimeout(() => {
console.log('2 seconds later');
}, 2000);

这两段代码的作用是一样的,都是延迟2秒后执行一个函数。

Dart 异步执行时的 Isolate vs JavaScript 的 Web Worker

由于我在实际开发中并没有使用到多线程的场景,所以这里先标记为TBD

作用域与闭包

Dart中的作用域和JavaScript中的作用域是非常类似的,比如:

1
2
3
4
5
6
7
void main() {
var name = 'Bob';
void sayName() {
print(name);
}
sayName();
}
1
2
3
4
5
6
7
function main() {
var name = 'Bob';
function sayName() {
console.log(name);
}
sayName();
}

这两段代码的作用是一样的,都是在函数内部定义一个变量,然后在内部函数中使用这个变量。最终输出的结果都是Bob,这就说明Dart的作用域是词法作用域。

wiki对词法作用域的定义是这样的:
静态作用域又叫做词法作用域,采用词法作用域的变量叫词法变量。词法变量有一个在编译时静态确定的作用域。
词法变量的作用域可以是一个函数或一段代码,该变量在这段代码区域内可见(visibility);在这段区域以外该变量不可见(或无法访问)。词法作用域里,取变量的值时,会检查函数定义时的文本环境,捕捉函数定义时对该变量的绑定。

关于闭包,Dart中的闭包和JavaScript中的闭包是非常类似的,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function makeCounter() {
int count = 0; // 外部变量

return () {
count++; // 闭包可以访问和修改外部变量
print(count);
};
}

void main() {
var counter = makeCounter(); // 获取闭包

counter(); // 输出 1
counter(); // 输出 2
counter(); // 输出 3
}

Dart 闭包的核心特点:

  1. 可以访问外部作用域的变量,即使外部作用域已结束
  2. 保留对外部变量的引用,而不是拷贝
  3. 每次调用闭包时,变量的状态会被保留

私有成员(_前缀)

Dart中类的私有成员是通过_前缀来实现的,比如:

1
2
3
4
class MyClass {
String _name = 'Bob';
String myName = 'Jake';
}

当我们在类外部访问这个私有成员时,会报错,比如:

1
2
3
4
5
void main() {
var obj = MyClass();
print(obj._name); // 这里会报错
print(obj.myName); // 这里不会报错
}

类与对象

Dart中的类和对象与JavaScript中的类和对象是非常类似的,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
String name;
int age;

Person(this.name, this.age);

void sayName() {
print(name);
}
}

void main() {
var p = Person('Bob', 18);
p.sayName();
}

extends 和 implements

Dart中的继承和实现与JavaScript中的继承和实现是非常类似的,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
String name;
int age;

Person(this.name, this.age);

void sayName() {
print(name);
}
}

class Student extends Person {
String school;

Student(String name, int age, this.school) : super(name, age);
}

Student stu = Student('Bob', 18, 'MIT');
stu.sayName(); // 输出 Bob

implements的用法也是类似的,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 普通类
class Person {
String name;
int age;

Person(this.name, this.age);

void sayName() {
print(name);
}
}

class Student implements Person {
@override
String name;
@override
int age;

String school;

Student(this.name, this.age, this.school);

void sayName() {
print(name);
}
}

Student stu = Student('Bob', 18, 'MIT');
stu.sayName(); // 输出 Bob

// 抽象类
abstract class Person {
String name;
int age;

Person(this.name, this.age);

void sayName() {
print(name);
}
}

class Student implements Person {
@override
String name;
@override
int age;

String school;

Student(this.name, this.age, this.school);

void sayName() {
print(name);
}
}

Student stu = Student('Jake', 18, 'MIT');
stu.sayName(); // 输出 Jake

extends和implements的区别在于,extends是继承一个类(继承了父类的所有非私有成员),而implements是实现一个接口(包括了普通类以及抽象类)。

Dart 的 mixin

Dart中的mixin是一种不能被实例化的类,它可以被其他类使用with关键字来继承,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
mixin Logger {
void log(String message) {
print('[LOG]: $message');
}
}

mixin TimeLogger {
void logTime() {
print('[LOG]: ${DateTime.now()}');
}
}

class MyClass with Logger, TimeLogger {
void doSomething() {
log("执行任务中...");
}

void when() {
logTime();
}
}

MyClass cls = MyClass();

cls.doSomething(); // 输出: [LOG]: 执行任务中...
cls.when(); // 输出: [LOG]: 2025-02-11 14:35:08.000

可以看出mixin可以给类添加一些额外的功能,这在实际开发中是非常有用的,尤其是flutter默认就给提供了很多的mixin,例如SingleTickerProviderStateMixin等。

静态方法与实例方法

Dart中的静态方法和实例方法与JavaScript中的静态方法和实例方法是非常类似的,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass {
String name;

MyClass(this.name);

void sayName() {
print(name);
}

static void sayHello() {
print('Hello');
}
}

void main() {
var obj = MyClass('Bob');
obj.sayName();
MyClass.sayHello();
}

类里的静态方法是通过static关键字修饰的,实例方法则没有修饰符,并且子类继承父类时,静态方法是不会被继承的。

库与包管理

Dart中的库和包管理与JavaScript中的模块化系统是非常类似的,比如:

1
2
3
import 'dart:math'; // Dart内置库
import 'package:flutter/material.dart' show Colors; // 只导入Colors
import 'package:flutter/material.dart' hide Colors; // 只过滤掉Colors

包管理是通过pubspec.yaml文件来管理的,类似与JavaScript中的package.json,这里是一份比较典型的pubspec.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
name: play_flutter
description: "A new Flutter project."

publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1

environment:
sdk: ^3.5.4

dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8

dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0

flutter:
uses-material-design: true

这里的dependencies是用来管理项目的依赖,dev_dependencies是用来管理项目的开发依赖,flutter是用来配置flutter项目的一些信息。