Flutter 基础 | Dart 语法

5,802

该系列记录了从零开始学习 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 中intdouble是两个有关数量的内建类型,它们都是num的子类型。

若声明变量为num,则可同时被赋值为intdouble

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]

条件插入

iffor是两个条件表达式,用于有条件的向列表中插入内容:

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的构造方法,其中的:表示初始化列表,初始化列表用来初始化成员变量,每一个初始化赋值语句用,隔开。

初始化列表的调用顺序是最高的,在一个类实例化时会遵循如下顺序进行初始化:

  1. 初始化列表
  2. 父类构造方法
  3. 子类构造方法

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

若在类成员变量前增加getset关键词,表示需要自定义该变量的存取逻辑:

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文件中)。定义在类中的方法没有可见性修饰符public private protected ,而是简单的以下划线区分,_开头的函数及变量是私有的,否则是公有的。

可选参数 & 命名参数

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 2elseimport 2show 1
as 2enuminstatic 2
assertexport 2interface 2super
async 1extendsisswitch
await 3extension 2late 2sync 1
breakexternal 2library 2this
casefactory 2mixin 2throw
catchfalsenewtrue
classfinalnulltry
constfinallyon 1typedef 2
continueforoperator 2var
covariant 2Function 2part 2void
defaultget 2required 2while
deferred 2hide 1rethrowwith
doifreturnyield 3
dynamic 2implements 2set 2

参考

Language tour | Dart

推荐阅读