Dart语言快速上手

Dart作为Flutter的开发语言,与其他语言有很多相似之处,我们来看一下快速上手Dart语言的相关知识。

表达式

创建并初始化变量:

1
var name = 'Bob';

显式的声明类型:

1
String name = 'Bob';

如果一个对象不受限于单一类型,可以指定为 Object 类型(或在必要时使用 dynamic)。

空安全

空安全能够防止意外访问 null 的变量而导致的错误。

空安全引入了三个关键更改:

  1. 当你为变量、参数或另一个相关组件指定类型时,可以控制该类型是否允许 null 。要让一个变量可以为空,你可以在类型声明的末尾添加 ?
1
2
3
String? name  // Nullable type. Can be `null` or string.

String name // Non-nullable type. Cannot be `null` but can be string.
  1. 你必须在使用变量之前对其进行初始化。可空变量是默认初始化为 null 的。 Dart 不会为非可空类型设置初始值,它强制要求你设置初始值。 Dart 不允许你观察未初始化的变量。这可以防止你在接收者类型可以为 nullnull 不支持的相关方法或属性的情况下使用它。
  2. 你不能在可空类型的表达式上访问属性或调用方法。同样的例外情况适用于 null 支持的属性或方法,例如 hashCodetoString()

空安全将潜在的 运行时错误 转变为 编辑时 分析错误。当非空变量已经是以下情况之一时,空安全标记为非空变量:

  • 使用非空值不初始化。
  • 赋值为 null

默认值

具有可空类型的未初始化变量的初始值为 null 。即使是具有数值类型的变量,初始值也为空,因为数字(就像 Dart 中的其他所有东西一样)都是对象。

1
2
int? lineCount;
assert(lineCount == null);

对于空安全,你必须在使用非空变量之前初始化它们的值.你不必在声明变量时初始化变量,但在使用之前需要为其赋值。

1
2
3
4
5
6
7
8
9
int lineCount;

if (weLikeToCount) {
lineCount = countLines();
} else {
lineCount = 0;
}

print(lineCount);

顶级变量和类变量是延迟初始化的,它们会在第一次被使用时再初始化。

延迟初始化

late 修饰符有两种用法:

  • 声明一个非空变量,但不在声明时初始化。
  • 延迟初始化一个变量。

如果你确定变量在使用之前已设置,但 Dart 推断错误的话,可以将变量标记为 late 来解决这个问题:

1
2
3
4
5
6
late String description;

void main() {
description = 'Feijoada!';
print(description);
}

当一个 late 修饰的变量在声明时就指定了初始化方法,那么内容会在第一次使用变量时运行初始化。

终值和常量

如果你不打算更改一个变量,可以使用 finalconst 修饰它,而不是使用 var 或作为类型附加。一个 final 变量只能设置一次,const 变量是编译时常量。(const 常量隐式包含了 final。)

下面是创建和设置 final 变量的示例:

1
2
final name = 'Bob'; // Without a type annotation
final String nickname = 'Bobby';

你不能修改 final 变量的值:

1
name = 'Alice'; // Error: a final variable can only be set once.

请使用 const 修饰 编译时常量 的变量。如果 const 变量位于类级别,请将其标记为 static const(静态常量)。

类型

内置类型有:

  • Numbers(int, double)
  • Strings(String)
  • Booleans(bool)
  • Records((value1, value2))
  • Lists(List, also known as arrays)
  • Sets(Set)
  • Maps(Map)
  • Runes(Runes)
  • Symbols(Symbol)
  • The value null(Null)

常见类型就不说了,看一下Records类型:

Recordsdart3.0及以上版本可用。Records是不可变的,只有getter而没有setter

语法:以逗号分隔的命名字段或位置字段列表,用小括号包裹:

1
2
3
4
5
6
var record = ('first', a: 2, b: true, 'last');
// 读取字段:命名字段有同名的getter,位置字段用$position
print(record.$1); // 'first'
print(record.a); // 2
print(record.b); // true
print(record.$2); // 'last'

dart中范型的用法与TypeScript比较像。

typedef可以定义类型别名,比如:

1
2
3
4
// 定义类型别名IntList
typedef IntList = List<int>;

IntList l = [1, 2, 3];

类型别名也可以使用范型:

1
2
3
4
5
typedef ListMapper<X> = Map<X, List<X>>;

ListMapper<String> m = {
"key": "value",
};

函数方法

Dart是面向对象的语言,所以即使是函数也是对象,并且有一个类型Function。这意味着函数可以分配给变量或作为参数传递给其他函数。

1
2
3
4
5
6
7
8
9
10
11
12
// 函数示例
int sum(int a, int b) {
return a + b;
}

// 省略类型也生效
sum(int a, int b) {
return a + b;
}

// 箭头函数
int sum(int a, int b) => a + b;

=> expr 语法是 { return expr; } 的简写。=> 表示法有时称为箭头语法。

参数

一个函数可以具有任意数量的必需位置参数。这些参数后面可以跟着命名参数或可选的位置参数(但不能同时跟这两个参数)。

在向函数传递参数或定义函数参数时,可以使用尾随逗号。

命名参数

命名参数是可选的,除非它们被显式标记为required.

定义函数时,请使用 {param1, param2, …} 指定命名参数。如果未提供默认值或将命名参数标记为required,则其类型必须为 null,因为其默认值为 null:

1
void enableFlags({bool? bold, bool? hidden}) {...}

可选位置参数

将一组函数参数包装在 [] 中将它们标记为可选位置参数。如果未提供默认值,则其类型必须可为 null,因为其默认值将为 null:

1
2
3
4
5
6
7
String say(String from, String msg, [String? device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}

main函数

每个应用都必须有一个顶级 main() 函数,该函数用作应用的入口点。main() 函数返回 void 并具有可选的 List<String> 参数参数。

下面是一个简单的 main() 函数:

1
2
3
void main() {
print('Hello, World!');
}

下面是接受参数的命令行应用的 main() 函数示例:

1
2
3
4
5
6
7
8
// Run the app like this: dart args.dart 1 test
void main(List<String> arguments) {
print(arguments);

assert(arguments.length == 2);
assert(int.parse(arguments[0]) == 1);
assert(arguments[1] == 'test');
}

匿名函数

匿名函数看起来类似于命名函数,即零个或多个参数,括号之间用逗号和可选的类型注释分隔。

下面的代码块包含函数的主体:

1
([[Type] param1[, …]]) { codeBlock;};

流程控制

if, for, while, break, continue等关键字及用法,与其他语言基本一致,不过多赘述,给几个示例:

标准for循环迭代:

1
2
3
for (int i = 0; i < 5; i++) {
print(i);
}

forEach

1
2
3
4
List<int> coll = [1, 2, 3];
coll.forEach((element) {
print(element);
});

whiledo-chile

1
2
3
4
5
6
7
while(条件){
doSomething();
}

do {
doSomething()
} while(条件);

if语句:

1
2
3
4
5
6
7
if(bool) {

} else if(bool) {

} else {

}

switch语句

1
2
3
4
5
6
7
8
switch(expr) {
case '1':
doSomething();
case '2':
doSomething();
default:
doSomething();
}

类和对象

Dart是一种面向对象的语言,具有类和基于mixin的继承。每个对象都是一个类的实例,除Null之外的所有类都派生自Object。基于mixin的继承意味着尽管每个类(除了顶层类Object)只有一个超类,但类体可以在多个类层次结构中重用。

类成员

对象的成员由方法和实例变量组成

1
2
3
4
5
var p = Point(2, 2);
// 读取变量
print(p.x);
// 调用方法
p.distanceTo(Point(4, 4));

可以使用?.调用来避免左侧操作数为null时发生异常:

1
var x = p?.x;

构造函数

创建与类名同名的函数来声明构造函数,初始化形式参数以实例化任何实例变量。

1
2
3
4
5
6
class Point {
double x = 0;
double y = 0;
// 构造函数,接收形参以实例化成员变量
Point(this.x, this.y);
}

使用命名构造函数为一个类实现多个构造函数,或提供额外的清晰度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const double xOrigin = 0;
const double yOrigin = 0;

class Point {
final double x;
final double y;

Point(this.x, this.y);

// Named constructor
Point.origin()
: x = xOrigin,
y = yOrigin;
}

请记住,构造函数不是继承的,这意味着超类的命名构造函数不是由子类继承的。如果要使用超类中定义的命名构造函数创建子类,则必须在子类中实现该构造函数。

您可以使用构造函数创建对象。构造函数名称可以是 ClassNameClassName.identifier。例如,以下代码使用 PointPoint.fromJson() 构造函数创建 Point() 对象:

1
2
var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});

以下代码具有相同的效果,但在构造函数名称之前使用可选的 new 关键字:

1
2
var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});

静态变量

使用 static 关键字实现类范围的变量和方法。

静态变量(类变量)对于类范围的状态和常量很有用:

1
2
3
4
5
6
7
8
class Queue {
static const initialCapacity = 16;
// ···
}

void main() {
assert(Queue.initialCapacity == 16);
}

静态变量在使用之前不会初始化。

并发

Dart 库充满了返回 FutureStream 对象的函数。这些函数是异步的:它们在设置可能耗时的操作(如 I/O)后返回,而无需等待该操作完成。

asyncawait 关键字支持异步编程,允许您编写类似于同步代码的异步代码。

当您需要完成的 Future 的结果时,您有两种选择:

  • 使用 asyncawait
  • 使用 Future API

使用 asyncawait 的代码是异步的,但它看起来很像同步代码。例如,下面是一些使用 await 等待异步函数结果的代码:

1
await lookUpVersion();

若要使用 await,代码必须位于async步函数中,即标记为async的函数:

1
2
3
4
Future<void> checkVersion() async {
var version = await lookUpVersion();
// Do something with version
}

使用 trycatchfinally 处理使用 await 的代码中的错误和清理:

1
2
3
4
5
try {
version = await lookUpVersion();
} catch (e) {
// React to inability to look up the version
}

您可以在async函数中多次使用 await

补充

补充一些注意事项,如果你是从其他语言转过来的,可能用得上:

dart中可以使用!做类型断言不为空,比如data.name!

类中的方法以_开头表示不对外暴露,仅内部可访问,这个内部指的是文件,即这个文件中都可以访问,但是其他文件不可以访问。

dart中有类似于js中模板字符串的用法:

  • 渲染变量: 'xxx $variable xxx' - 直接用$变量即可
  • 进行运算:'xxx ${var1 + var2} xxx' - 用${表达式}渲染

函数中的位置参数如果不标记为可选都是必传的,通过{}包裹的命名参数如果不使用required修饰则都是可选的

dart中构造函数默认不能自定义返回值,如果需要可以使用工厂构造,比如Color.fromRGBO这些就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Chat {
final String name;
final String message;
final String imageUrl;

Chat({
required this.name,
required this.message,
required this.imageUrl,
});
// 工厂构造, 可以自定义返回值
factory Chat.fromMap(Map mapData) {
return Chat(name: mapData['name'], message: mapData['message'], imageUrl: mapData['imageUrl']);
}
}

dart中的类实例才可以a.b这样访问属性,但是在map中需要a[b]这样访问。

这是一些dart的快速上手内容,了解了之后,就可以直接写起来了,在写的过程中,才能慢慢深入了解dart这门语言。

作者

胡兆磊

发布于

2024-03-18

更新于

2024-04-07

许可协议