该系列记录了从零开始学习 Flutter 的学习路径,第一站就是 Dart 语法。本文可以扫除看 Flutter 教程,写 Flutter 代码中和语言有关的绝大部分障碍。值得收藏~
声明并初始化变量
int i = 1; // 非空类型必须被初始化 int? k = 2; // 可空类型 int? h; // 只声明未初始化,则默认为 null var j = 2; // 自动推断类型为int late int m; // 惰性加载 final name = 'taylor'; // 不可变量,name 不能再次被赋值 final String name = 'taylor'; // 不可变量
Dart 中语句的结尾是带有分号;
的。
Dart 中声明变量时可以选择是否为它赋初始值。但非空类型必须被初始化。
Dart 中声明变量可以显示指明类型,类型分为可空和非空,前者用类型?
表示。也可以用var
来声明变量,此时编译器会根据变量初始值自动推断类型。
late
关键词用于表示惰性加载,它让非空类型惰性赋值成为可能。得在使用它之前赋值,否则会报运行时错误。
惰性加载用于延迟计算耗时操作,比如:
late String str = readFile();
str 的值不会被计算,直到它第一次被使用。
??
可为空类型提供默认值
String? name; var ret = name ?? ''
如果 name 为空则返回空字串,否则返回 name 本身。
数量
在 Dart 中int
和double
是两个有关数量的内建类型,它们都是num
的子类型。
若声明变量为num
,则可同时被赋值为int
或double
num i = 1; i = 2.5;
字串
''
和""
都可以定义一个字串
var str1 = 'this is a str'; var str2 = "this is another str";
字串拼接
使用+
拼接字符串
var str = 'abc'+'def'; // 输出 abcdef
多行字串
使用'''
声明多行字符串
var str = ''' this is a multiple line string ''';
纯字串
使用r
声明纯字符串,其中不会发生转义。
// 输出 this is a raw \n string var str = r'this is a raw \n string';
字串内嵌表达式
字符串中可以内嵌使用${}
来包裹一个有返回值的表达式。
var str = 'today is ${data.get()}';
字串和数量相互转化
int.parse('1'); // 将字串转换为 int double.parse('1.1'); // 将字串转换为 double 1.toString(); // 将 int 转换为字串 1.123.toStringAsFixed(2); // 将 double 转换为字串,输出 '1.12'
集合
声明 List
与有序列表对应的类型是List
。
用[]
声明有序列表,并用,
分割列表元素,最后一个列表元素后依然可以跟一个,
以消灭复制粘贴带来的错误。
var list = [1,2,3,];
存取 List 元素
列表是基于索引的线性结构,索引从 0 开始。使用[index]
可以获取指定索引的列表元素:
var first = list[0]; // 获取列表第一个元素 list[0] = 1; //为列表第一个元素赋值
展开操作符
...
是展开操作符,用于将一个列表的所有元素展开:
var list1 = [1, 2, 3]; var list2 = [...list1, 4, 5, 6];
上述代码在声明 list2 时将 list1 展开,此时 list2 包含 [1,2,3,4,5,6]
除此之外,还有一个可空的展开操作符...?
,用于过滤为null
的列表:
var list; // 声明时未赋初始值,则默认为 null var list2 = [1, ...?list]; // 此时 list2 内容还是[1]
条件插入
if
和for
是两个条件表达式,用于有条件的向列表中插入内容:
var list = [ 'aa', 'bb', if (hasMore) 'cc' ];
如果 hasMore 为 true 则 list 中包含'cc',否则就不包含。
var list = [1,2,3]; var list2 = [ '0', for (var i in list) '$i' ];// list2 中包含 0,1,2,3
在构建 list2 的时候,通过遍历 list 来向其中添加元素。
Set
Set
中的元素是可不重复的。
用{}
声明Set
,并用,
分割元素:
var set = {1,2,3}; // 声明一个 set 并赋初始元素 var set2 = <Int>{}; // 声明一个空 set var set3 = new Set(); // 声明一个空 set var set4 = Set(); // 声明一个空 set,new 关键词可有可无
Map
Map
是键值对,其中键可以是任何类型但不能重复。
var map = { 'a': 1, 'b': 2, }; // 声明并初始化一个 map,自动推断类型为 Map<String,Int> var map2 = Map<String,Int>(); // 声明一个空 map map2['a'] = 1; // 写 map var value = map['a']; //读 map
读写Map
都通过[]
。
const & final
const
是一个关键词,表示一经赋值则不可修改:
// list var list = const [1,2,3]; list.add(4); // 运行时报错,const list 不可新增元素 // set var set = const {1,2,3}; set.add(4); // 运行时报错,const set 不可新增元素 // map var map = const {'a': 1}; map['b'] = 2; // 运行时报错,const map 不能新增元素。
它和 final 的区别在集合上体现的很明显:
final numbers = [1,2,3]; numbers.add(4); // 没有问题 numbers = [4,5,6]; // 报错‘*The final variable can only be set once.’ const nums = [1,2,3]; nums.add(4);// 报错‘Cannot add to an unmodifiable list’ nums = [4,5,6];// 报错‘Constant variables can’t be assigned a value’
final 表示引用不可变,而内容是可变的。
const 表示内容和引用都不可变
类
声明类
class Pointer { double x; double y; void func() {...} // void 表示没有返回值 double getX(){ return x; } }
- 用关键词
class
声明一个类。
- 类体中用
类型 变量名;
来声明类成员变量。
- 类体中用
返回值 方法名(){方法体}
来声明类实例方法。
继承
class Pointer {} class SubPointer extends Pointer {}
继承使用关键词extends
构造方法
上述代码会在 x ,y 这里报错,说是非空字段必须被初始化。通常在构造方法中初始化成员变量。
构造方法是一种特殊的方法,它返回类实例且签名和类名一模一样。
class Point { double x = 0; double y = 0; // 带两个参数的构造方法 Point(double x, double y) { this.x = x; this.y = y; } }
这种给成员变量直接赋值的构造方法有一种简洁的表达方式:
class Point { double x = 0; double y = 0; Point(this.x, this.y); // 当方法没有方法体时,得用;表示结束 }
命名构造方法
Dart 中还有另一个构造方法,它的名字不必和类名一致:
class Point { double x; double y; Point.fromMap(Map map) : x = map['x'], y = map['y']; }
为 Point 声明一个名为fromMap
的构造方法,其中的:
表示初始化列表,初始化列表用来初始化成员变量,每一个初始化赋值语句用,
隔开。
初始化列表的调用顺序是最高的,在一个类实例化时会遵循如下顺序进行初始化:
- 初始化列表
- 父类构造方法
- 子类构造方法
Point.fromMap() 从一个 Map 实例中取值并初始化给成员变量。
然后就可以像这样使用命名构造方法:
Map map = {'x': 1.0, 'y': 2.0}; Point point = Point.fromMap(map);
命名构造方法的好处是可以将复杂的成员赋值的逻辑隐藏在类内部。
继承构造方法
子类的构造方法不能独立存在,而是必须调用父类的构造方法:
class SubPoint extends Point { SubPoint(double x, double y) {} }
上述 SubPointer 的声明会报错,提示得调用父类构造方法,于是改造如下:
class SubPoint extends Point { SubPoint(double x, double y) : super(x, y); }
在初始化列表中通过super
调用了父类的构造方法。父类命名构造方法的调用也是类似的:
class SubPoint extends Point { SubPoint(Map map) : super.fromMap(map); }
构造方法重定向
有些构造方法的目的只是调用另一个构造方法,为此可以在初始化列表中通过this
实现:
class Point { double x = 0; double y = 0; Point(this.x, this.y); Point.onlyX(double x): this(x, 0); }
Point.onlyX() 通过调用另一个构造方法并为 y 值赋值为 0 来实现初始化。
getter setter
若在类成员变量前增加get
或set
关键词,表示需要自定义该变量的存取逻辑:
class Pointer { double x; double y; double get distance;// 抽象方法 Pointer(this.x, this.y); }
因为类不是抽象的,而却包含一个抽象方法,此时编译器会报错“ distance 必须有一个方法体,因为 Pointer 并不是抽象的”。可以这样改:
abstract class Pointer { double x; double y; double get distance; Pointer(this.x, this.y); }
若不想抽象化,也可以直接就地实现:
class Pointer { double x; double y; double get distance { return sqrt(x * x + y * y); } Pointer(this.x, this.y); }
distance 的语义是“坐标点与原点的距离”。在 Dart 中这些基于现有属性求值而来的额外属性,通常被实现为 getter。
distance 的求值只有一行代码,也可以用如下简洁的方式表达:
class Pointer { double x; double y; double get distance => sqrt(x * x + y * y); Pointer(this.x, this.y); }
该功能在 AndroidStudio 中有一个快捷键,Alt + Enter > Convert to expression body。
然后就可以像这样读取这个成员:
var pointer = Pointer(3,4); pointer.distance; // 5
getter 也常被用于私有成员访问器:
class Pointer { double _x; // 私有成员 double _y; // 私有成员 Pointer(this._x, this._y); double get x => _x; // 私有成员访问器 double get y => _y; // 私有成员访问器 }
默认情况下,公有类成员变量(不以_
开头的变量)都有一个隐含的 getter 方法,即直接返回变量。
setter 也是同理:
class Pointer { double x; double y; set moveBy(double delta);// 抽象方法,会报错 Pointer(this.x, this.y); }
这次换一种方式,将抽象方法的实现延迟到子类:
abstract class Pointer { double x; double y; set moveBy(double delta); Pointer(this.x, this.y); } class SubPointer extends Pointer { SubPointer(double x, double y) : super(x, y); // 实现父类抽象的 set @override set moveBy(double delta) { x += delta; y += delta; } }
此时 moveBy
的语义是将“将坐标点平移 delta 个单位”。然后就可以像这样为它赋值:
var pointer = Pointer(3,4); pointer.moveBy = 2; // Pointer(5,6)
定义额外 setter 和 getter 的意义在于 “把复杂的赋值和取值逻辑包装在一个变量的存取表达式中”。 这是一件功德无量的事情,它降低了上层代码的复杂度。
重载运算符
class Pointer { double x; double y; Pointer(this.x, this.y); operator +(p) => Pointer(this.x + p.x, this.y + p.y); }
使用关键词operator
表示重载运算符,这里重载的+
,即重新定义加号运算符,然后就可以像这样使用:
var p1 = Pointer(1,2); var p2 = Pointer(3,4); var p3 = p1 + p2; // Pointer(4,6)
方法
Dart 中方法也是一种类型,对应Function
类,所以方法可以被赋值给变量或作为参数传入另一个方法。
// 下面声明的两个方法是等价的。 bool isValid(int value){ return value != 0; } isValid(int value){// 可自动推断返回值类型为 bool return value != 0; }
声明一个返回布尔值的方法,它需传入一个 int 类型的参数。
其中方法返回值bool
是可有可无的。
bool isValid(int value) => value != 0;
如果方法体只有一行表达式,可将其书写成单行方法样式,方法名和方法体用=>
连接。
Dart 中的方法不必隶属于一个类,它也可以顶层方法的形式出现(即定义在.dart文件中)。定义在类中的方法没有可见性修饰符publicprivateprotected,而是简单的以下划线区分,_
开头的函数及变量是私有的,否则是公有的。
可选参数 & 命名参数
Dart 方法可以拥有任意数据的参数,对于非必要参数,可将其声明为可选参数,调用方法时,就不用为其传入实参:
bool isValid(int value1, [int value2 = 2, int value3 = 3]){...}
定义了一个具有两个可选参数的方法,其中第二三个参数用[]
包裹,表示是可选的。而且在声明方法时为可选参数提供了默认值,以便在未提供相应实参时使用。所以如下对该方法的调用都是合法的。
var ret = isValid(1) // 不传任何可选参数 var ret2 = isValid(1,2) // 传入1个可选参数 var ret3 = isValid(1,2,3) // 传入2个可选参数
使用[]
定义可选参数时,如果想只给 value1,value3 传参,则无法做到。于是乎就有了{}
:
bool isValid(int value1, {int value2 = 2, int value3 = 3}) {...}
然后就可以跳过 value2 直接给 value3 传参:
var ret = isValid(1, value3 : 3)
这种语法叫可选命名参数。
Dart 还提供了关键词required
指定在众多可选命名参数中哪些是必选的:
bool isValid(int value1, {int value2, required int value3}) {...}
匿名方法
匿名方法表示在给定参数上进行一顿操作,它的定义语法如下:
(类型 形参) { 方法体 };
如果方法体只有一行代码可以将匿名函数用单行表示:
(类型 形参) => 方法体;
静态方法
static fun1(){ ... }
使用static
关键词标记方法为静态的,它不隶属于类实例,所以也无法访问类成员。
异常
使用throw
抛出异常:
throw StateError('invalid state.');
在 Dart 中任何类型都可以被抛出,比如下面抛出一个字符串:
throw "wrong input";
使用try-on-catch-finally
捕获异常:
try { // 可能发生异常的语句 } on TimeoutException { // 捕获超时异常 } catch (e) { // 捕获任何异常 } finally { // 不管异常有没有发生这里都会执行 }
操作符
三元操作符
三元操作符格式如下:布尔值 ? 表达式1 : 表达式2;
var ret = isValid ? 'good' : 'no-good';
如果 isValid 为 true 则返回表达式1,否则返回表达式2。
瀑布符
该操作符..
用于合并在同一对象上的多个连续操作:
val paint = Paint() ..color = Colors.black ..strokeCap = StrokeCap.round ..strokeWidth = 5.0
构建一个画笔对象并连续设置了 3 个属性。
如果对象可控则需使用?..
:
paint?..color = Colors.black ..strokeCap = StrokeCap.round ..strokeWidth = 5.0
类型判定操作符
as
是强转操作符,表示将一个类型强转为另一个类型。
is
是类型判定操作符,用于判断某个实例是否是指定类型。
is!
是与 is
相反的判定。
流程控制
if-else
if (isRaining()) { you.bringRainCoat(); } else if (isSnowing()) { you.wearJacket(); } else { car.putTopDown(); }
for
for (var i = 0; i < 5; i++) { message.write('!'); }
如果不需要关心循环的索引值,则可以这样:
for (var item in list) { item.do(); }
while
while (!isDone()) { doSomething(); }
do { printLine(); } while (!atEndOfPage());
break & continue
break & continue 可用于 for 和 while 循环。
break
用于跳出循环
var i = 0 while (true) { if (i > 2) break; print('$i'); i++; } // 输出 0,1,2
continue
用于跳过当前循环的剩余代码:
for (int i = 0; i < 10; i++) { if (i % 2 == 0) continue; print('$i'); }// 输出 1,3,5,7,9
switch-case
Dart 中的 switch-case 支持 String、int、枚举的比较,以 String 为例:
var command = 'OPEN'; switch (command) { case 'CLOSED': case 'PENDING': // 两个 case 共用逻辑 executePending(); break; // 必须有 break case 'APPROVED': executeApproved(); break; case 'DENIED': executeDenied(); break; case 'OPEN': executeOpen(); break; default: // 当所有 case 都未命中时执行 default 逻辑 executeUnknown(); }
关键词
所有的关键词如下所示:
abstract 2 | else | import 2 | show 1 |
as 2 | enum | in | static 2 |
assert | export 2 | interface 2 | super |
async 1 | extends | is | switch |
await 3 | extension 2 | late 2 | sync 1 |
break | external 2 | library 2 | this |
case | factory 2 | mixin 2 | throw |
catch | false | new | true |
class | final | null | try |
const | finally | on 1 | typedef 2 |
continue | for | operator 2 | var |
covariant 2 | Function 2 | part 2 | void |
default | get 2 | required 2 | while |
deferred 2 | hide 1 | rethrow | with |
do | if | return | yield 3 |
dynamic 2 | implements 2 | set 2 |