Dart

Table of Contents

1. Dart 简介

Dart is a client-optimized language for fast apps on any platform.

Dart 最开始是 Google 设计出来替代 Javascript 的,但是并没有凑效,后来 Flutter 选择了 Dart, 才使 Dart 活跃起来。

参考:
A tour of the Dart language: https://dart.dev/guides/language/language-tour
Dart cheatsheet codelab: https://dart.dev/codelabs/dart-cheatsheet
Dart style guide: https://dart.dev/guides/language/effective-dart/style
Dart documentation: https://dart.dev/guides

2. 语法简介

下面是一个简单的 dart 程序:

// Define a function.
void printInteger(int aNumber) {
  print('The number is $aNumber.'); // Print to console.
}

// This is where the app starts executing.
void main() {
  var number = 42; // Declare and initialize a variable.
  printInteger(number); // Call a function.
}

2.1. 变量

使用 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'
};

也可以明确指定类型,如:

var name = 'Bob';            // 不明确指定类型,类型推导系统可推导出类型
String name = 'Bob';         // 同上

如果不想把变量限定为单一类型,可以使用 Object 或者 dynamic 类型,如:

dynamic name = 'Bob';
Object name = 'Bob';

dynamic 和 Object 的区别可参考:https://dart.dev/guides/language/effective-dart/design#do-annotate-with-object-instead-of-dynamic-to-indicate-any-object-is-allowed

2.1.1. late 变量

在 Dart 2.12 中引入了 late 修饰符,它有两个作用:

  1. 变量的初始化工作可在声明之后进行;
  2. Lazily initializing a variable.

下面是一个例子:

late String description;

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

如果直接使用一个没有初始化的变量,将导致运行时错误。

2.1.2. 常量(final, const)

使用 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

此外, const 声明常量时,要求这个常量必须是编译时就能确定的常量。

2.2. 内置类型

Dart 中支持下面这些内置类型:

  • int 整数
  • double 浮点数
  • String 字符串
  • bool 布尔值
  • List 列表(数组)
  • Set 集合
  • Map 映射
  • Runes and grapheme clusters
  • Symbol 符号

参考:https://dart.dev/guides/language/language-tour#built-in-types

2.2.1. List

下面是 List 的实例:

var list = [10, 20, 30];              // 创建 List,类型推导得到的类型为 List<int>
assert(list.length == 3);
assert(list[0] == 10);                // 下标从 0 开始,即首元素的下标是 0
assert(list[1] == 20);

list.add(40);                         // 使用 add 往 list 中增加一个元素

list[1] = 10;                         // 直接修改 List 中数据
assert(list[1] == 10);

使用 spread operator (...) 可以由一个 List 创建另一个 List:

var list = [1, 2, 3];
var list2 = [0, ...list];
assert(list2.length == 4);

上面例子中,如果 list 为 null,则会报错,这时可以使用 null-aware spread operator (...?) 避免这个问题:

var list;
var list2 = [0, ...?list];            // list 为 null,也不会报错
assert(list2.length == 1);

在构造 list 时,可以使用 if,如:

var nav = [
  'Home',
  'Furniture',
  'Plants',
  if (promoActive) 'Outlet'
];

在构造 list 时,可以使用 for,如:

var listOfInts = [1, 2, 3];
var listOfStrings = [
  '#0',
  for (var i in listOfInts) '#$i'
];
assert(listOfStrings[0] == '#0');
assert(listOfStrings[1] == '#1');
assert(listOfStrings[2] == '#2');
assert(listOfStrings[3] == '#3');

2.2.2. Set

下面是 Set 的例子:

var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};

下面是使用 Set 构造函数创建 Set 的例子:

var names = <String>{};
// Set<String> names = {};   // This works, too.
// var names = {};           // Creates a map, not a set.

names.add('fluorine');       // 往 set 中增加元素
names.addAll(halogens);      // 往 set 中增加多个元素

当不特别指定时,Set 默认为 LinkedHashSet,你也可以显式指定使用 HashSet 或者 SplayTreeSet

2.2.3. Map

下面是 Map 的例子:

var gifts = {                         // 类型推导为 Map<String, String>
  // Key:    Value
  'first': 'partridge',
  'second': 'turtledoves',
  'fifth': 'golden rings'
};

var nobleGases = {                    // 类型推导为 Map<int, String>
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

下面是使用 Map 构造函数创建 Map 的例子:

var gifts = Map<String, String>();
gifts['first'] = 'partridge';          // Add a key-value pair
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';

var nobleGases = Map<int, String>();
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';

当不特别指定时,Map 默认为 LinkedHashMap(维持插入顺序),你也可以显式指定使用 HashMap 或者 SplayTreeMap(一种自平衡树,最近访问的元素可以在再次访问时更快)。

2.2.4. null safety

在 Dart 2.12 中引入了 null safety:

//int a = null;         // 从 Dart 2.12 起,这是不合法的。int 不能为 null
int? a = null;          // 类型后加个 ? 表示,这个变量可能为 null

a = 42;
int b = a!;             // ! 是 Null assertion operator,从 int? 类型的 a 得到了 int 类型的 b

同时引入了一些 Null-aware operators,如表 1 所示。

Table 1: Null-aware operators
记号 说明
Null-aware assignment operator ??= a ??= 3 是赋值语句,当变量 a 为 null 时赋值为 3;不为 null 就维持原值
Conditional expressions ?? a ?? 5 是表达式, a 为 null 时表达式为 5;不为 null 时表达式是原值
Conditional property access .? myObject?.someProperty 相当于 (myObject != null) ? myObject.someProperty: null
Null-aware cascade operator ?.. 见下文
Null-aware index operator ?[] items?[0] 相当于 items != null ? items[0] : null

下面介绍一下 .? ,假设有代码:

querySelector('#confirm')    // Get an object.
  ?..text = 'Confirm'        // Use its members.
  ..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!'));

参考:https://dart.dev/null-safety/understanding-null-safety

2.3. 函数

下面是函数的例子:

bool isNoble(int atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

可以省略类型,即上面定义还可以写为:

isNoble(atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

如果函数体中只有一个表达式,则还可以进一步使用“箭头语法”简化它,如上面定义还可以写为:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

箭头语法 => expr{ return expr; } 的简写。

2.3.1. 命名参数(大括号)

定义函数时,参数使用 {param1, param2, …} 的形式(即参数用大括号包围起来)就是命名参数,如:

void func1({String arg1, bool arg2}) {
  print(arg1);
  print(arg2);
}

main() {
  func1(arg1: "abc", arg2: true);     // 调用时,需要指定参数的名称 arg1,arg2
  func1();                            // 如果指定参数的话,函数体内会为 null
}

命名参数是可选的,不指定时,函数体内会得到 null 。执行上面程序,会输出:

abc
true
null
null

2.3.2. 可选的位置参数(中括号)

如果把位置参数放到中括号中,则该位置参数是可选的。如下面例子中的参数 device 是可选的:

String say(String from, String msg, [String device]) { // 参数 device 在中括号中,是可选的
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  return result;
}

main() {
  assert(say('Bob', 'Howdy') == 'Bob says Howdy');
  assert(say('Bob', 'Howdy', 'smoke signal') == 'Bob says Howdy with a smoke signal');
}

2.3.3. 参数默认值

= 可以指定参数的默认值。如:

/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold = false, bool hidden = false}) {...}

// bold will be true; hidden will be false.
enableFlags(bold: true);

2.3.4. 匿名函数

匿名函数的语法如下:

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

下面代码中 list.forEach 的参数是一个匿名函数:

main() {
  var list = ['apples', 'bananas', 'oranges'];
  list.forEach((item) {
        print('${list.indexOf(item)}: $item');
  });
}

如果函数体中只有一个表达式,可以使用箭头语法,如:

  list.forEach((item) => print('${list.indexOf(item)}: $item'));

2.3.5. 函数返回值

All functions return a value. If no return value is specified, the statement return null;

2.4. 操作符

Dart 操作符如表 2 所示。

Table 2: Dart 操作符
Description Operator
unary postfix expr++ expr-- () [] . ?.
unary prefix -expr !expr ~expr ++expr --expr await expr
multiplicative * / % ~/
additive + -
shift << >> >>>
bitwise AND &
bitwise XOR ^
bitwise OR |
relational and type test >= > <= < as is is!
equality == !=
logical AND &&
logical OR ||
if null ??
conditional expr1 ? expr2 : expr3
cascade ..
assignment = *= /= += -= &= ^= etc.

2.4.1. Type test operators (as, is, is!)

Dart 中可以使用 as, is, is! 在运行时对对象类型进行检查,如表 3 所示。

Table 3: Type test operators
Operator Meaning
as Typecast
is obj is T is true if obj implements the interface specified by T
is! obj is! T is true if obj doesn’t implement the interface specified by T

下面是使用 as 把 employee 转换为 Person 的例子:

(employee as Person).firstName = 'Bob';

2.4.2. Cascade notation (..)

操作符 .. 可以方便地让你在一个相同对象上进行一系列操作。如:

querySelector('#confirm')      // Get an object.
  ..text = 'Confirm'           // Use its members.
  ..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!'));

在这个例子中,操作符 .. 让我们省去了定义临时变量 button 的过程。

2.5. 控制流语句

2.5.1. if, else

下面是 if 语句的例子:

if (isRaining()) {
  you.bringRainCoat();
} else if (isSnowing()) {
  you.wearJacket();
} else {
  car.putTopDown();
}

2.5.2. for

下面是 for 语句的例子:

var message = StringBuffer('Dart is fun');
for (var i = 0; i < 5; i++) {
  message.write('!');
}

List 和 Set 还支持 for-in 形式的遍历:

var collection = [0, 1, 2];
for (var x in collection) {
  print(x); // 0 1 2
}

2.5.3. while, do-while

下面是 while 语句的例子:

while (!isDone()) {
  doSomething();
}

下面是 do-while 语句的例子:

do {
  printLine();
} while (!atEndOfPage());

2.5.4. switch, case

下面是 switch 语句的例子:

var command = 'OPEN';

switch (command) {
  case 'CLOSED':
    executeClosed();
    break;
  case 'PENDING':
    executePending();
    break;
  case 'APPROVED':
    executeApproved();
    break;
  case 'DENIED':
    executeDenied();
    break;
  case 'OPEN':
    executeOpen();
    break;
  default:
    executeUnknown();
}

2.6. 异常

使用 try/catch/finally 可以进行异常处理,使用 on 可以捕获特定的异常。如:

void main() {
  try {
    var result = 100 ~/ 0;      // throws `IntegerDivisionByZeroException` exception
    print( 'result: $result' );
  } on IntegerDivisionByZeroException { // 这里也可以加上 catch(e)
    print("Error: can not divide by 0.");
  } on FormatException catch( e ) {     // 如果只关心异常类型,而不关心细节,则 catch(e) 可以不写
    print("Error: format is not correct, $e");
    rethrow;                            // Allow callers to see the exception.
  } catch( e, s ) {                     // catch 可以指定一个或者两个参数
    print('Exception details:\n $e');
    print('Stack trace:\n $s');
  } finally {                           // finally 中代码总会执行
    print( 'Job Done!' );
  }
}

我们可以使用 throw 来抛出异常,如:

void distanceTo(Point other) => throw UnimplementedError();

2.7.

Dart 中使用 class 定义类,类中可以包含实例变量和方法,如:

import 'dart:math';

class Point {                         // 定义一个名为 Point 的类
  double x = 0;                       // Point 实例变量,默认为 0
  double y = 0;                       // Point 实例变量,默认为 0

  Point(double x, double y) {         // Point 构造函数,可以使用语法糖省写为 Point(this.x, this.y);
    this.x = x;
    this.y = y;
  }

  Point.origin()                            // Point 的另一种形式的构造函数,称为 Named constructor
      : x = 0,                              // 冒号后面的 x = 0, y = 0 是 initializer list
        y = 0 {}                            // 这对空大括号,可以写为分号 ;

  Point.fromJson(Map<String, double> json)  // Point 的另一个 named constructor
      : x = json['x']!,
        y = json['y']! {
    print('In Point.fromJson(): ($x, $y)');
  }

  double distanceTo(Point other) {          // Point method
    var dx = x - other.x;
    var dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
}

void main() {
  var p1 = Point(2, 2);

  // Get the value of y.
  assert(p1.y == 2);

  // Invoke distanceTo() on p.
  double distance = p1.distanceTo(Point(4, 4));
  print(distance);  // 2.8284271247461903

  var p2 = Point.origin();
  assert(p2.y == 0);

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

2.7.1. Factory constructors

前面的例子中,介绍了普通构造函数和 Named 构造函数(如 Point.origin 和 Point.fromJson),这里再介绍另一种构造函数:Factory constructor。

Factory constructor 可以返回子类型甚至 null,如:

class Square extends Shape {}

class Circle extends Shape {}

class Shape {
  Shape();

  factory Shape.fromTypeName(String typeName) {   // 这个构造函数返回子类型
    if (typeName == 'square') return Square();
    if (typeName == 'circle') return Circle();

    print('I don\'t recognize $typeName');
    throw Error();
  }
}

2.7.2. Getters and setters

每个成员变量都隐式地有一个 get 方法和一个可能存在的 set 方法(对于常量就没有 set)。我们也可以自己定义 Getters and setters,下面是一个例子:

class Rectangle {
  double left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  // Define two calculated properties: right and bottom.
  double get right => left + width;
  set right(double value) => left = value - width;
  double get bottom => top + height;
  set bottom(double value) => top = value - height;
}

void main() {
  var rect = Rectangle(3, 4, 20, 15);
  assert(rect.left == 3);
  rect.right = 12;
  assert(rect.left == -8);
}

2.7.3. Abstract class

使用关键字 abstract 可以定义一个抽象类,这样的类不能被实例化,抽象类往往用于定义接口。下面是抽象的例子:

// This class is declared abstract and thus
// can't be instantiated.
abstract class AbstractContainer {              // 这是一个抽象类
  // Define constructors, fields, methods...

  void updateChildren();                        // Abstract method.
}

2.7.4. Implicit interfaces (implements)

每个类都“隐式”定义了一个接口,这个接口包含所有实例成员(方法和变量)。下面是一个例子:

// A person. The implicit interface contains greet().
class Person {
  // In the interface, but visible only in this library.
  final _name;

  // Not in the interface, since this is a constructor.
  Person(this._name);

  // In the interface.
  String greet(String who) => 'Hello, $who. I am $_name.';
}

// An implementation of the Person interface.
class Impostor implements Person {                          // Impostor 实现了 Person 接口
  get _name => '';

  String greet(String who) => 'Hi $who. Do you know who I am?';
}

String greetBob(Person person) => person.greet('Bob');

void main() {
  print(greetBob(Person('Kathy')));
  print(greetBob(Impostor()));
}

2.7.5. Extending a class (extends)

使用 extends 可以创建一个子类,下面是一个例子:

class Television {
  void turnOn() {
    _illuminateDisplay();
    _activateIrSensor();
  }
  // ···
}

class SmartTelevision extends Television {  // SmartTelevision 是 Television 的子类
  void turnOn() {
    super.turnOn();                         // super 关键字表示其父类
    _bootNetworkInterface();
    _initializeMemory();
    _upgradeApps();
  }
  // ···
}

2.7.6. Private 成员(名字以下划线开始)

Dart 中的 Class 成员,不使用 public/private 等关键字修饰,默认都是外部可访问的。如果想定义不能被外部访问的 Private 成员,则其成员名字以下划线 _ 开始即可。

3. 包管理

Dart 生态中使用 package 来管理可复用的公共代码(可称为包),可以从 https://pub.dev/ 中查找公开可用的 Dart 包。

3.1. Package 使用实例

下面以 http 为例,介绍一下 Dart 中 Package 的使用。

首先,准备 pubspec.yaml 文件,把 http 作为依赖引入进来,内容如下:

name: my_app
environment:
  sdk: '>=2.10.0 <3.0.0'

dependencies:
  http: ^0.13.0

然后,执行 dart pub get 下载依赖。

使用 Package 时,在 import 语句中以 package: 为前缀导入 pubspec.yaml 中指定的包,如:

import 'dart:convert' as convert;
import 'package:http/http.dart' as http;               // 引入 http

void main(List<String> arguments) async {
  // This example uses the Google Books API to search for books about http.
  // https://developers.google.com/books/docs/overview
  var url = Uri.https('www.googleapis.com', '/books/v1/volumes', {'q': '{http}'});

  // Await the http get response, then decode the json-formatted response.
  var response = await http.get(url);                  // 使用 http
  if (response.statusCode == 200) {
    var jsonResponse = convert.jsonDecode(response.body) as Map<String, dynamic>;
    var itemCount = jsonResponse['totalItems'];
    print('Number of books about http: $itemCount.');
  } else {
    print('Request failed with status: ${response.statusCode}.');
  }
}

Author: cig01

Created: <2019-01-29 Tue>

Last updated: <2021-04-07 Wed>

Creator: Emacs 27.1 (Org mode 9.4)