Dart 语言之旅教程(完整版)

Flutter 踩坑之旅之 Dart 学习笔记


写在前面

本文是参考了 Dart 官网https://www.dartlang.org 的教程、参考的机翻以及对于其他语言学习的经验来翻译的本入门教程,译者精力和学历有限,如有不妥之处欢迎指正。

本文出于知识共享的目的,转载请注明出处,如果用于商用请联系 admin@jieec.cn
本文也发布在了杰易创绎 Jiee Create http://www.jieec.cn/?p=228
本文 PDF 版部分文档内容可以在 Jieec Server 中获取 https://server.jieec.cn

在线编译

DartPad https://dartpad.dartlang.org/

概念

  • Dart 变量中的内容都是对象,对象都是类的实例,所有对象都是从 Object 类继承的
  • Dart 要求规定类型,但是可以自行推断类型。var num = 10; 就可以推断其类型为 int(不需要类型之 dynamic
  • Dart 之泛型类型 List <int> (整数列表)or List <dynamic>(任何类型的对象列表
  • 主函数 main(), 支持自定义函数和函数嵌套
  • 支持全局变量,私有变量。实例的变量可以称为字段或者属性
  • 不具备数据保护相关的关键字,用(_) 开头确定私有
  • 标识符可以以字母或者(_)下划线开头,后面字母和数字任意组合

关键字

关键字列表
abstract 2 dynamic 2 implements 2 show 1
as 2 else import 2 static 2
assert enum in super
async 1 export 2 interface 2 switch
await 3 external 2 is sync 1
break extends 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 rethrow while
deferred 2 hide 1 return with
do if set 2 yield 3

避免使用关键字作为标识符,如果有必要标数字的关键字可以作为标识符:

  • 1:上下文关键字标识符,仅在特定位置有含义
  • 2:内置标识符,简化 js 转移 dart 的工作,不能作为类或者类型的标识符,不能用作导入前缀
  • 3:dart 1.0 发布后添加的异步支持相关的更新,有限的保留字。不能使用 await or yield 作为任何函数体中的标识符标记 async,async* or sync*

变量

* var(属于 Object)& dynamic 的辨析

  • var 可以被解析为任意类型,但是一旦确定就不可更改!
  • dynamic 的值不可以被编译器解析或者类型检查
  • dynamic 编译到 Object 中,dynamic 编译时存在,运行时不存在

默认值

未初始化的默认值都为 null,断言assert(condition);

final & const

不打算更改变量的值用 final 或者 const 定义变量。final 变量只能被设置一次;const 变量是编译时常量(const 变量是隐式 final 的)。final 的全局变量和类变量第一次使用的时候要被初始化。

注意:在实例变量中只可以用 final 而非 const。必须在构造函数体之前初始化 final 变量(在变量声明,构造函数参数或者构造函数的初始化列表中。)

示例:

  final name = 'Bob';//无法再更改值

如果 const 变量在类内,请标记为 static const,const 属于 Object 类

  const bar = 1000000;//压力单位(dynes/cm2)
  const double atm = 1.01325*bar;//标准大气压

const 不仅仅可以声明常变量,也可以用来创建常量值 constant values,以及声明常量值的构造函数,任何变量都可以具有常量值。

  var foo = const [];
  final bar = const [];
  const baz = [];//与"const []"等价
  • 可以省略声明 const 的初始化表达式,如上所示。不要冗余使用 const!
  • 你可以修改非 final,非 const 变量的值,即使变量曾用过 const 的值
  foo = [1,2,3];√
  baz = [1,2,3];×

内置类型

  • 数字
  • 字符串
  • 布尔型
  • 列表(数组)
  • 地图 map
  • 符文 runes(用于在字符串中表示 Unicode 字符)
  • 符号 symbols

因为 Dart 中的每个变量都引用一个对象 - 一个类的实例 - 您通常可以使用构造函数来初始化变量。一些内置类型有自己的构造函数。例如,您可以使用 Map() 构造函数来创建地图。 ——来源于官网引用

数字

  1. int

整数值不大于 64 位,具体视平台而定

  1. double

64 位双精度浮点数,IEEE 754 标准

int & double 都属于数字这一类型,数字基本运算符有 +、-、*、/ 之类的,也包括 abs()、ceil()、floor() 和其他方法。(类似于 ">>" 的位运算符,在 int 类中被定义),如果在 num 及其子类中没有要找的,去 dart:math 库中看看!

int 示例

  int x = 1;
  int hex = 0xDEADBEEF;

double 示例

  double y = 1.1;
  double exponents = 1.42e5;

以下是字符串如何转化为数字,反之亦然

  // String -> int
  var one = int.parse('1');
  assert(one == 1);

  // String -> double
  var onePointOne = double.parse('1.1');
  assert(onePointOne == 1.1);

  // int -> String
  String oneAsString = 1.toString();
  assert(oneAsString == '1');

  // double -> String
  String piAsString = 3.14159.toStringAsFixed(2);
  assert(piAsString == '3.14');

int 类型指定移位操作(«、»),AND(&)和 OR(|)运算符。

  assert((3<<1)==6);// 0011 << 1 == 0110
  assert((3>>1)==1);// 0011 >> 1 == 0001
  assert((3|4)==7);// 0011 | 0100 == 0111

Literal numbers(能力有限,不知道如何翻译)是编译时常量,很多的算术表达式也是编译时的常量,只要它们的操作数是编译为数字的编译时常量——引用自 dart 官网

  const msPerSecond = 1000;
  const secondsUntilRetry = 5;
  const msUntilRetry = secondsUntilRetry*msPerSecond;

字符串

Dart 字符串是一系列的 UTF-16 代码单元,可以使用 '' or"" 创建字符串。

示例

  var s1 = 'Single quotes work well for string literals.';
  var s2 = "Double quotes work just as well.";
  var s3 = 'It\'s easy to escape the string delimiter.'; // 译者注\'是转译输出'
  var s4 = "It's even easier to use the other delimiter.";

你可以将表达式的值置入字符串中通过使用 ${expression}。如果表达式是标识符,你可以跳过 {}(就是省略{})。要获取会对象对应的字符串,Dart 会调用对象的 toString() 方法。

  var s = 'string interpolation';

  assert('Dart has $s, which is very handy.' == 'Dart has string interpolation, ' + 'which is very handy.');
  assert('That deserves all caps. ' + '${s.toUpperCase()} is very handy!' == 'That deserves all caps. ' + 'STRING INTERPOLATION is very handy!');

"==" 运算符是测试两个对象是否是等价的,如果它们包含相同的代码单元序列,那么就是等价的。

你可以使用运算符’+’连接字符串(译者省略了演示代码)

另一种创建多行字符串的方法是:使用三个单引号或者双引号

  var s1 = '''
  多行字符串1
  ''';

  var s2 = """
  多行字符串2
  """;

你可以通过添加前缀 r 来创建原始字符串(在原始字符串中,字符串是不会被特殊处理的)

  var s = r'这是一个原始字符串';

有关如何在字符串中表示 Unicode 字符的详细信息,请参阅Runes

只要任何插值表达式是一个编译时常量,其值为 null 或数值,字符串或布尔值;文字字符串即为编译时常量。

演示代码

  // 常字符串
  const aConstNum = 0;
  const aConstBool = true;
  const aConstString = 'a constant string';

  // 并非常字符串
  var aNum = 0;
  var aBool = true;
  var aString = 'a string';

  const aConstList = [1, 2, 3];
  const validConstString = '$aConstNum $aConstBool $aConstString';
  // const invalidConstString = '$aNum $aBool $aString $aConstList';

获取更多的字符串的使用信息,请参考字符串与正则表达式

布尔型

Dart 用 bool 类表示布尔值,只有两个对象 true、false,它们都是编译时常量。

Dart 的类型安全意味着你不能使用类似于 if(非布尔值) 或者 assert(非布尔值) 的语句,不过可以明确地检查值,如示例:

  // 检查空字符串
  var fullName = '';
  assert(fullName.isEmpty);

  // 检查零
  var hitPoints = 0;
  assert(hitPoints <= 0);

  // 检查null
  var unicorn;
  assert(unicorn == null);

  // 检查NaN
  var iMeantToDoThis = 0 / 0;
  assert(iMeantToDoThis.isNaN);

列表

几乎每种语言中最常见的是数组或者有序对象组。在 Dart 中数组是 List 对象,因此大多人也称为列表。

Dart 列表看起来想 JavaScript 数组。示例:

  var list = [1,2,3];

分析器推断 list 类型为 list。如果将非 int 类型对象加入此列表,会引发错误。获取更多的信息,阅读有关 类型推断

列表从零开始索引,0 是列表的第一个元素索引,list.length - 1 是列表的最后一个元素索引。可以像 JavaScript 那样获取一个列表的长度并引用元素。

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

  list[1] == 1;
  assert(list[1] == 1);

创建一个编译时常量列表,在列表文字前添加 const:

  var constantList = const [1,2,3]
  // 同样,值不可以更改

List 类型有很多方便的方法来操作列表。有关更多信息,请参阅 泛型 Generics集合 Collections

地图型 Maps

译者注:根据译者对于其他语言的学习和开发经验,Dart 语言的 Maps 型和 Python 中的字典是一样的,其中键就是 key,值就是 value。

通常,map 型是一个有键值对构成的对象。键和值可以是任意类型的对象。键不能重复,而同样的值是可以的。Dart 对于 map 的支持是通过地图文字和 Map 类型。

以下是一些简单使用 map 文字创建的 Dart maps:

  var gifts = {
    // key:value
    'first': 'partridge',
    'second': 'turtledoves',
    'fifth': 'golden rings',
  };

  var nobleGases = {
    2: 'helium',
    10: 'neon',
    18: 'argon',
  };

解析推断 gifts 类型为 Map<String,String>,nobleGases 的类型为 Map<int,String>。如果尝试去添加非对应类型的值,解析器或者运行的时候会报错。获取更多的信息,请参阅 类型推断

你也可以使用构造函数 Map() 创建相同的对象:

  var gifts = Map();
  gifts['first'] = 'partridge';
  gifts['second'] = 'turtledoves';

  // 不想写了,省略···

你可能想看到的是 new Map(),而非 Map()。从 Dart 2 开始,new 关键字是可选的。有关详细信息,请参阅 使用构造函数

跟在 JavaScript 中一样,将新的键值对加到现有的地图中(译者注:在 python 的字典中其实也是这样的···

  var gifts = {'first': 'partridge'};
  gifts['fourth'] = 'calling birds';

跟在 JavaScript 中一样的方式在地图中检索值,不在就返回 null:(译者注:译者懒得注了

  var gifts = {'first': 'partridge'};
  assert(gifts['first'] == 'partridge');
  assert(gifts['fifth'] == null);

使用.length 获取 map 中键值对的数目

  var gifts = {'first': 'partridge'};
  gifts['fourth'] = 'calling birds';
  assert(gifts.length == 2);

常地图,定义与上面相似,不写了···

  final constantMap = const {
    2: 'helium',
    10: 'neon',
    18: 'argon',
  };
  // const改都报错,后面就不写了

获取更多的信息,请参阅 泛型 Generics地图型 Maps

符文

在 Dart 中,符文是字符串中 UTF-32 代码点

Unicode 为世界上所有书写系统中的每一个字母、数字、符号都定义了唯一的数值。由于 Dart 字符串是一系列的 UTF-16 代码单元,因此在字符串中表示 32 位要用特殊的语法。

表达 Unicode 代码点的常用的方法是 \uXXXX,其中 XXXX 是 4 位十六进制值。例如心(💗)是 \u2665。要指定多于或者少于 4 个十六进制值,请将值放在大括号里。例如,笑脸(😆)是 \u{1f600}。

该字符串类有几个属性,你可以用它来提取符文信息。codeUnitAt 和 codeUnit 属性返回 16 位编码单元。使用该 runes 属性获取字符串的符文。

以下示例说明了符文,16 位代码单元和 32 位代码点之间的关系。

  main()
  {
    var clapping = '\u{1f44f}';
    print(clapping);
    print(clapping.codeUnits);
    print(clapping.runes.toList());

    Runes input = new Runes(
      '\u2665  \u{1f605}  \u{1f60e}  \u{1f47b}  \u{1f596}  \u{1f44d}');
    print(new String.fromCharCodes(input));
  }

注意:使用列表操作符文要小心,这种方法容易分解,具体取决于特定的语言,字符集和操作。更多信息,请在 Stack Overflow 上参阅 如何在 Dart 中反转字符串? ——引用自 dart 官网

符号 Symbols

符号对象表示 Dart 程序的操作或者声明。你可能不会去使用符号对象,但是它们对于 API 通过名称引用是非常有用的,因为缩小会更改标识符名称而不会更改标识符符号。(译者并不清楚官网这句话的具体含义,引用自 Dart 官网

请使用符号文字获取标识符的符号(# 后面跟着标识符):

  #radix
  #bar

符号文字是编译时常量!

函数 Function

Dart 是一门面向对象的语言,即便是函数也是对象,并且具有类型 Function 。这意味着函数可以被赋值给变量,或者作为参数传递给其他函数。你也可以跟调用函数一样,调用 Dart 类的实例。有关信息请参阅 可调用类 Callable classes

以下是一个实现函数的示例:

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

虽然 Effective Dart 建议 为公共 API 键入注释 ,但是省略类型,函数还是有效的

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

对于只含有一个表达式的函数,可以使用简写的语法:

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

该语法是速记,有时候称为 箭头语法 。=> expr {return expr;}

只有 表达式 而非 语句 可以出现在 “=>” 和 “;” 之间。例如,不能在其中放入 if 语句,但可以使用条件表达式。 (译者注:也就是类似于其他语言也有的条件判断表达式比如 ? : 和 if 语句的区别)

函数可以有两种类型的参数:必需和可选。首先列出所需的参数,然后列出任何可选参数。命名的可选参数也可以标记为 @required 。有关详细信息,参阅下一节。

可选参数

可选参数可以是位置参数,也可以是命名参数,不局限于这些。

可选命名参数

调用函数的时候,可以使用 paramName: value 指定的命名参数。例如:

  enableFlags(bold: true, hidden: false);

定义函数时,使用 {param1, param2, …} 指定命名参数:

  // 设置[bold]和[hidden]标志flags...
  void enableFlags({bool bold, bool hidden}){...}

Flutter 实例创建表达式变得会更复杂,因此窗口小部件构造函数仅使用命名参数,使实例创建表达式更易于阅读。

可以使用 @required 在任何 Dart 代码(不仅仅是 Flutter)中注释命名参数,以指示它是 必需参数 。例如:

  const Scrollbar({Key key, @required Widget child})

当一个构造 Scrollbar,分析器会报错 child 不存在。

必需 Required元 meta 包中被定义。可以直接导入 package:meta/meta.dart ,也可以导入另一个导出的包 meta,例如 Flutter 的包 package:flutter/material.dart

可选位置参数

包装一组函数参数在 [] 内标记它们作为可选位置参数:

  String say(String from, String msg, [String device]){
    var result = '$from says $msg';
    if (device != null){
      result = '$result with a $device';
    }
    return result;
  }

下面是一个没有可选参数情况下调用此函数的示例:

  assert(say('Bob', 'Howdy') == 'Bob says Howdy');

下面是一个带有第三个参数情况下调用此函数的示例:

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

默认参数值

你的函数可以使用 “=” 给命名或者位置参数定义默认值。默认值必须是编译时常量。如果未设默认值,则默认值为 null。

这是一个为命名参数设置默认值的示例:

  // 设置[bold]和[hidden]flag
  void enableFlags({bool bold = false,bool hidden = false}) {...}

  // bold值为true hidden为false
  enableFlags(bold: true);

在老代码中使用冒号取代等号设置命名参数,原因是原生的问题。只有冒号支持命名参数。这种支持遭到了反对,建议使用等号设置默认值。

这个示例演示了如何去给位置参数设置默认值

  String say(String from, String msg, [String device = 'carrier pigeon', String mood]){
    var result = '$from says $msg';
    if (device != null){
      result = '$result with a $device';
    }
    if (mood != null){
      result = '$result (in a $mood mood)';
    }
    return result;
  }

  assert(say('Bob', 'Howdy') == 'Bob says Howdy with a carrier pigeon');

你也可以将列表或者地图作为默认值传递。下面的示例定义了一个 doStuff() 的函数,为 list 参数指定了一个默认的列表,为 gifts 参数指定了一个默认的地图。

  void doStuff(
  {List<int> list = const [1, 2, 3],
    Map<String, String> gifts = const {
      'first': 'paper',
      'second': 'cotton',
      'third': 'leather'
    }
  }){
    print('list: $list');
    print('gifts: $gifts');
  }

main() 函数

每一个 app 都必须有个顶级函数 main(),相当于是 app 的入口。main() 函数返回 void 并且有一个可选的 List参数。

这是一个 web app 的 main() 函数示例:

  void main(){
    querySeletor('#sample_text_id')
      ..text = 'Click me!'
      ..onClick.listen(reverseText);
  }

.. 这个先于代码的语法称为 级联 。使用级联,你可以对单个对象的成员执行多个操作。

下面是一个带参数的 main 函数 () 的命令行 app 示例:

  // 像这样运行app: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');
  }

你可以使用 args 库 去定义和分析命令行参数。

函数作为第一类对象

你可以将一个函数作为参数传递给其他函数。比如:

  void printElement(int element){
    print(element);
  }

  var list = [1, 2, 3];

  // printElement作为参数传递
  list.forEach(printElement);

你也可以将函数分配给一个变量。比如:

  var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
  assert(loudify('hello') == '!!! HELLO !!!');

这个例子使用了一个匿名函数。想了解更多,请看下节教程。

匿名函数

大多的函数都是被命名过的,比如 main()或者 printElement()。你也可以创建一个无名函数叫做 匿名函数 ,有时候也叫做 lambda 或者 closure 。比如你可以给一个变量指定一个匿名函数,以便你可以从一个集合中添加或者删除。

匿名函数和一个命名函数看起来很相似——没有或者数个参数,在逗号和括号之间用逗号和可选类型注释分隔。

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

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

下面的例子中定义了一个带有未指定类型参数 item 的匿名函数。为列表中的每个项调用的函数将打印一个字符串,该字符串包含指定索引处的值。

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

写入程序的输出结果为:
1. 0: apples
2. 1: bananas
3. 2: oranges

如果函数只包含了一个语句,你可以使用箭头表示法让代码更短。复制下面的代码到 DartPad,然后点击 run 证明它在功能上是等价的。

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

词汇作用域 Lexical scope

Dart 是一个有词汇作用域的语言,就意味着变量的作用域是静态定义的,只需通过代码的布局。你可以 “顺着花冠向外”译者觉得这可能是一个比喻)去看变量是否在作用域范围内。

以下是在每个范围级别具有变量的嵌套函数的示例:

  bool topLevel = true;

  void main() {
    var insideMain = true;

    void myFunction() {
      var insideFunction = true;

      void nestedFunction() {
        var insideNestedFunction = true;

        assert(topLevel);
        assert(insideMain);
        assert(insideFunction);
        assert(insideNestedFunction);
      }
    }
  }

注意 nestedFunction() 是如何使用每一级的变量,一直到顶级。

词汇闭包 Lexical closures

闭包 closure 是一个在它的词汇范围有权访问变量的函数对象,即使函数原来的范围之外。

函数关闭在周围定义的变量。在下面的示例中,makeAdder() 捕获变量 addBy。无论返回的函数在哪里,它都会记住 addBy。

  // 返回一个函数,它为函数的参数添加[addBy]
  Function makeAdder(num addBy){
    return (num i) => addBy + i;
  }

  void main(){
    // 创造一个函数添加2
    var add2 = makeAdder(2);

    // 创造一个函数添加4
    var add4 = makeAdder(4);

    assert(add2(3) == 5);
    assert(add4(3) == 7);
  }

测试函数是否等价

下面是一个测试顶级函数,静态方法,实例方法等价性的例子:

  void foo() {} // 顶级函数
  class A {
    static void bar() {} // 静态方法
    void baz() {} // 实例方法
  }

  void main() {
    var x;

    // 比较顶级函数
    x = foo;
    assert(foo == x);

    // 比较静态方法
    x = A.bar;
    assert(A.bar == x);

    // 比较实例方法
    var v = A(); // A的实例#1
    var w = A(); // A的实例#2
    var y = w;
    x = w.baz;

    // 这些闭合参照的是一个实例(#2),所以它们是等价的
    assert(y.baz == x);

    // 这些闭合参照的是不同的实例,所以它们不等价
    assert(v.baz != w.baz);
  }

返回值

所有的函数都会返回值。如果没有返回值被指定,表达式返回 null;隐式添加到函数体。

  foo() {}
  assert(foo() == null);

运算符

Dart 定义了下述表格中的运算符。你可重载部分的运算符,在 重载运算符 中具体介绍。

类型 运算符
一元后缀 expr++ expr– ()[] . ?.
一元前缀 -expr !expr ~expr ++expr –expr
乘式 * / % ~/
和式 + -
移位 « »
位与 &
位异或 ^
位或
译者注:由于 markdown 的制表和位或运算的符号发生冲突,故使用的是丨(gun)代替
关系与类型测试 >= > <= as is is!
等价 == !=
逻辑与 &&
逻辑或 丨丨
译者注:同上
如果 null ??
控制表达式 expr1 ? expr2 : expr3
级联 ..
赋值 = *= /= ~/= %= += -= «= »= &= ^= 丨 = ??=

使用运算符可以创建表达式,下面是几个例子:

  a++
  a + b
  a = b
  a == b
  c ? a : b
  a is T

运算符表 中,每个运算符的优先级都高于后一行的优先级。例如 % 高于 == ,而 == 高于 && 。就意味着下面两行的代码执行方式是相同的。

  // 圆括号提高了可读性
  if ((n % i == 0) && (d % i == 0)) ...

  // 难读,但是是等价的
  if (n % i == 0 && d % i == 0)

注意:对于处理两个操作数的运算符,最左边的操作数确定运算符。——引用自官网

算术运算符

Dart 支持通用的算术运算符,具体如下表:

运算符 意义
+
-
-expr 一元减号,也表示否定(反转表达式符号)
*
/
~/ 除,返回整数结果
% 取余
  // ** 译者在此省略了部分演示代码**
  assert(5 / 2 == 2.5);
  assert(5 ~/ 2 == 2);
  assert(5 % 2 == 1);

  assert('5/2 = ${5 ~/ 2} r ${5 % 2}' == '5/2 = 2 r 1');

Dart 也支持带前缀和后缀的自加或自减运算 (具体的优先级及取值参照其他语言的基础

等价与关系运算符

运算符 意义
== 等于;看下面讨论
!= 不等于
> 大于
< 小于
>= 不小于
<= 不大于

去测试两个对象 x、y 代表相同的东西,使用 == 运算符。(在极少数的情况下,你需要知道两个对象是不是同一个对象,使用 identical() 函数代替。)下面展示了 == 是如何工作的。

  1. 如果 x 或者 y 为 null,如果他俩都是 null 返回 true,不然返回 false
  2. 返回方法调用的结果 x.==(y)。(这样是正确的,如同 == 的运算符是在第一个操作数上被调用的方法。你可以重载一些运算符,包括 == ,具体参考 重载运算符

举例略

键入测试运算符

在程序运行的时候,as、is、is! 运算符用于检查类型。

运算符 意义
as 类型转换
is 如果是指定类型返回 true
is! 如果不是指定类型返回 true

使用 as 运算符将对象强制转换为特定的类型。通常,应该使用它作为 is 对使用该对象的表达式后跟对象的测试的简写。

  if(emp is Person){
    // 检查类型
    emp.firstName = 'Bob';
  }

  // 你可以使用as让代码短一点
  (emp as Person).firstName = 'Bob';

这两个写法是不等价的,如果 emp 为 null 或者不是 Person,第一个例子啥都不干,第二个例子抛出异常。

赋值运算符

译者在这一部分省略了大量与其他语言重复的部分,只针对个别特殊的符号加以解析

  b ??= value; // 如果b为null,值将会赋给b;如果不为空,b还是原来的值

组合赋值运算符,略

逻辑运算符

运算符 意义
!expr 非运算
丨丨 或运算
&& 与运算

按位和移位运算符

运算之前,将值转化为二进制

译者按照 1 为真,0 为假编写解释

运算符 意义
& 全真则真,一假则假
一真则真,全假为假
^ 位异或运算,同为假,异为真
~expr 假为真,真为假
<< 左移位
>> 右移位

条件表达式

Dart 有两种运算符,可以简明使用 if-else 表达式

condition ? expr1 : expr2

真返回 1 结果,假返回 2 结果

expr1 ?? expr2

如果 expr1 非空,返回它的值;否则,返回 2 的结果

通过布尔表达式控制赋值,考虑?:

如果布尔表达式是用来测试是否为空的,考虑??

  String playerName(String name) => name ?? 'Guest';

前面的例子至少可以使用两种方式来编写,但是不简洁:

  // 精简使用?:
  String playerName(String name) => name != null ? name : 'Guest';

  // 使用if-else表达式写
  String playerName(String name){
    if (name != null){
      return name;
    }else{
      return 'Guest';
    }
  }

级联符号(..)

级联(..)允许你对同一个对象进行一系列的操作。除了函数的调用之外,你还可以访问同一对象上的字段。这通常可以节省创建临时变量的步骤,让代码书写更流畅。

  querySeletor('#confirm') // 获取一个对象
    ..text = 'Confirm' // 使用它的一个成员
    ..classes.add('important')
    ..onClick.listen((e) => window.alert('Confirmed!'));

第一个方法调用,querySeletor(),返回一个选择器对象。级联表示法后面的代码对此对象进行操作,忽略可能返回的后续值。

前面的示例相当于:

  var button = querySeletor('#confirm');
  button.text = 'Confirm';
  button.classes.add('import');
  button.onClick.listen((e) => window.alert('Confirmed!'));

你也可以嵌套你的级联:

  final addressBook = (AddressBookBuilder()
        ..name = 'jenny'
        ..email = 'jenny@example.com'
        ..phone = (PhoneNumberBuilder()
              ..number = '415-555-0100'
              ..label = 'home')
            .build())
      .build();

注意在返回实际函数的对象上构造级联。比如,以下代码失败:

  var sb = StringBuffer();
  sb.write('foo')
    ..write('bar'); // error:write方法没有定义为void

sb.write() 调用返回 void,你不能在 void 这一类函数上构造级联。

注:严格意义上来说,对于级联的两点符号并不是运算符,只是 Dart 语法的一部分。

其他类型运算符

在其他的示例中,你已经见过大多的剩余的运算符了:

运算符 名称 意义
() 函数应用 表示函数的调用
[] 列表访问 引用列表中指定索引处的值
. 成员访问权限 指表达式的属性;示例:fool.bar 从表达式 foo 中选择了属性 bar
?. 条件成员访问 跟 ‘.’ 类似,但是运算符的左边可以为 null,比如:foo?.bar 从 foo 中选择了 bar 属性即使 foo 为空(在这种情况下,foo?.bar 的值为 null)

了解更多的信息关于 . ,?. ,和 . 运算符,看 类 Classes

流程控制语句

你可以用下述的 Dart 代码控制流程:

  • if 和 else
  • for 循环
  • while 和 do-while 循环
  • break 和 continue
  • switch 和 case
  • assert

你也可以使用 try-catch 和 throw 影响控制流程,在 异常 Exceptions 中介绍。

if-else 语句

Dart 支持带有可选 else 的 if 语句,在下个例子中有演示。另见 条件表达式

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

不像 JavaScript,条件必须是布尔值,不能为其他。具体信息看 布尔

for 循环

你可以使用标准的 for 循环迭代。

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

在 Dart 中 for 循环内部的闭包捕获了索引的值,避免了 JavaScript 中常见的陷阱。

  var callbacks = [];
  for (var i = 0; i < 2; i++){
    callbacks.add(() => print(i));
  }
  callbacks.forEach((c) => c());

如预期那样,先输出 0 后 1。相反在 JavaScript 中这个例子先 2 后 2。

如果迭代的对象是可迭代的,你可以使用 forEach()方法。如果你不需要当前的迭代计数器,使用 forEach() 是个不错的选择。

  candidates.forEach((candidate) => candidate.interview());

类似于列表和集合这样的可迭代类也支持迭代的 for-in 结构:

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

while 和 do-while

while 控制条件在循环之前

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

do-while 控制条件在循环之后

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

break 和 continue

使用 break 停止循环

  while(true){
    if (shutDownRequested()) break;
    processIncomingRequests();
  }

使用 continue 跳到下一个循环迭代

  for (int i = 0; i < candidates.length; i++){
    var candidate = candidates[i];
    if (candidate.yearsExperience < 5){
      continue;
    }
    candidate.interview();
  }

如果你使用类似于列表、集合的 迭代 ,你可以以不同的方式写示例:

  candidates
      .where((c) => c.yearsExperience >= 5)
      .forEach((c) => c.interview());

switch 和 case

跟 C 语言的结构并没有什么本质的区别,略,case 内局部变量只在内部有效

断言 assert

如果布尔值为 false,使用 assert 语句将中断程序的正常执行。你可以在这愉快之旅中的例子中感受 assert 语句的用法。

  // 确认变量有个非空的值
  assert(text != null);

  // 确认值比100小
  assert(number < 100);

  // 确认是个https链接
  assert(urlString.startsWith('https'));

注:断言语句对生产代码并没有什么影响;它们只是用于开发。Flutter 允许断言在 debug 模式 。仅限开发的工具(比如 dartdevc)通常默认支持断言。一些工具如 dart,dart2js 通过命令行标志支持断言:–enable-asserts

将消息加到断言,添加字符串作为第二参数。

  assert(urlString.startsWith('https'),'URL ($urlString) should start with "https".');

第一个参数 assert 可以是任何解析为布尔值的表达式。如果表达式的值为 true,则断言成功并继续执行。如果为 false,则断言失败并抛出异常(一个 断言错误

异常 Exceptions

Dart 代码可以抛出和捕获异常,异常是指意外事件发生的错误。如果未能捕获异常,则会暂停引发异常的隔离,并且通常会终止隔离和程序。

与 Java 相比,所有的 Dart 异常都是未检查的异常。方法不会声明它们会抛出的异常,你也没必要捕获异常。

Dart 提供了 异常错误 两种类型,以及许多预定义的子类型,当然也可以自己定义的意外情况。Dart 程序可以抛出任何非空对象 - 不仅仅是异常和错误对象 - 作为例外。

抛出 throw

一个抛出或者引发异常的示例:

  throw FormatException('Expected at least 1 section');

你也可以任意对象:

  throw 'Out of llamas!';

注:生成有质量的代码通常会抛出 异常错误

因为抛出异常是一种表达,你可以使用 => 语句抛出异常,在任何可以表达的地方:

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

捕获 Catch

捕获,会阻止异常的传播(除非重新抛出异常)。捕获异常使有机会处理它:

  try{
    breedMoreLlamas();
  } on OutOfLlamasException {
    buyMoreLlamas();
  }

要处理可能抛出多种异常的代码,可以指定多个捕获子句。与抛出对象类型匹配的第一个子句处理异常。如果捕获子句未指定类型,则该子句可以处理任何异常:

  try{
    breedMoreLlamas();
  } on OutOfLlamasException {
    // 一个指定异常
    buyMoreLlamas();
  } on Exception catch (e) {
    // 其他任何异常
    print('Unkown exception: $e');
  } catch (e) {
    // 没有指定类型,可以处理所有
    print('Someting really unkown: $e');
  }

如上面的代码所示,你可使用 on 或 catch,或者一起用。使用 on 时需要指定异常的类型。使用 catch 时,你的异常处理程序需要异常对象。

你可以指定一个或者两个 catch() 参数。第一个抛出异常,第二个是堆栈跟踪(StackTrace 对象)

  try{
    // ...
  } on Exception catch (e) {
    print('Exception details:\n $e');
  } catch (e, s) {
    print('Exception details:\n $e');
    print('Stack trace:\n $s');
  }

要处理部分异常,并允许它传播,请使用关键字 rethrow。

  void misbehave() {
    try {
      dynamic foo = true;
      print(foo++); // runtime错误
    } catch (e) {
      print('misbehave() partially handled ${e.runtimeType}.');
      rethrow; // 允许调用者查看异常
    }
  }

  void main(){
    try{
      misbehave();
    } catch (e) {
      print('main() finished handling ${e.runtimeType}.');
    }
  }

最后 Finally

无论是否抛出异常,要确保某些代码的运行,请使用 finally 子句。如果没有 catch 子句匹配该异常,则在 finally 子句运行后传播异常。

  try{
    breedMoreLlamas();
  } finally {
    // 总是被清理,即使有其他异常
    cleanLlamaStalls();
  }

finally 子句在任何匹配的 catch 子句之后运行:

  try{
    breedMoreLlamas();
  } catch (e) {
    print('Error: $e'); // 首先处理异常
  } finally {
    cleanLlamaStalls(); // 然后清理
  }

了解更多,请阅读库之旅的 异常 部分。

Dart 是一门面向对象的语言,具有类以及基于 mixin 的继承。每个对象都是一个类的实例,所有的类都来自于 Object。基于 Mixin 的继承意味着即使每个类(除了 Object)只有一个超类(父类),但是类体可以在多个类层次结构中重用。

使用类成员

对象有由函数和数据(分别为方法和实例变量)组成的成员。调用方法时,可以在对象上调用它:该方法可以访问该对象的函数和数据。

使用点(.)来引用实例变量或者方法:

  var p = Point(2, 2);

  // 设置实例变量y的值
  p.y = 3;

  // 获取y的值
  assert(p.y == 3);

  // 在p上调用distanceTo()
  num distance = p.distanceTo(Point(4, 4));

使用 ?. 代替 . 避免最左边的操作数为空的情况

  // 如果p不为空,把它的y的值设为4
  p?.y = 4;

使用构造函数

你可以使用构造函数来创造对象。构造函数的名称可以是 ClassName 或 ClassName.identifier。举例,下面的代码创建了一个 Point 对象使用了构造函数 Point()和 Point.fromJson():

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

下面的代码的效果是一样的,但是在构造函数之前使用了可选的关键字 new

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

版本提示:在 Dart2 中 new 才是可选的

一些类提供了常构造函数。使用常构造函数去创建一个编译时的常量,在构造函数的名称前添加关键词 const

  var p = const ImmutablePoint(2, 2);

构造两个相同的编译时常量会产生一个规范的实例:

  var a = const ImmutablePoint(1, 1);
  var b = const ImmutablePoint(1, 1);

  assert(identical(a, b));// 它们是相同的实例

在常量的上下文中,你可以省略构造函数或者文字前的 const 关键字,比如,看这段创造一个常地图代码。

  // 这有太多的const关键字

  const pointAndLine = const {
    'point': const [const ImmutablePoint(0, 0)],
    'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
  };

你可以省略除了第一个 const 关键字

  // 只用一个const
  const pointAndLine = {
    'point': [ImmutablePoint(0, 0)],
    'line': [ImmutablePoint(1,10), ImmutablePoint(-2, 11)],
  };

如果一个常构造函数在常上下文外被调用没有 const,它创建了一个非 常对象

  var a = const ImmutablePoint(1, 1);// 创造了一个常对象
  var b = ImmutablePoint(1, 1);// 未创造一个常对象

  assert(!identical(a, b));// 它们不是相同的实例

版本提示:在 Dart2 中在常上下文中 const 才是可选的。

获取一个对象的类型

在程序执行的过程中获取对象的类型,你可以使用 Object 的 runtimeType 属性,返回 类型 Type 对象。

  print('The type of a is ${a.runtimeType}');

到目前为止,你已经了解如何使用类了。本节的其余部分将介绍如何实现类。

实例变量

这里展示了如何去声明一个实例变量:

  class Point{
    num x; // 声明实例变量x,初始值为空
    num y; // 声明变量y,初始值为空
    num z=0; // 声明变量,初始值为0
  }

没有初始化的变量的值为 null

所有的实例变量都生成一个隐式的 getter 方法。非最终实例变量(Non-final)也生成一个 setter 方法。获取更多的细节,参照 Getters 和 Setters

  class Point {
    num x;
    num y;
  }

  void main(){
    var point = Point();
    point.x = 4; // 对x使用setter方法
    assert(point.x == 4); // 对x使用getter方法
    assert(point.y == null); // 默认值为null
  }

如果你在实例对象声明的地方初始化对象(而不是在构造函数或者方法中),当实例被创建的时候值就已经被设置了,在构造函数和初始化列表执行之前。

构造函数

通过创建一个跟它的类同名的构造函数来实现构造函数的声明。(plus,可选的,还有一个额外的标识符,如同 命名构造函数 所述)

最常见的构造函数形式,即生成构造函数,创造类的一个新实例:

  class Point {
    num x, y;
    Point (num x, num y) {
      // 有更好的方法去做这个

      this.x = x;
      this.y = y;
    }
  }

this 关键字指向现况的实例

使用 this 时这会有一个命名冲突。否则,Dart 代码将会省略 this。

将构造函数的值赋给实例变量的模式是很常见的。Dart 有语法 sugar 使实现更简单:

  class Point {
    num x, y;

    // 使用语法sugar设置x,y的值
    // 在构造函数体运行之前
    Point(this.x, this.y);
  }

默认构造函数

如果自己没有声明构造函数,它会自己生成一个。默认生成的构造函数没有参数,并在超类(父类)中调用无参构造函数。

构造函数不是被继承的

子类不从超类(父类)中继承构造函数。声明没有构造函数的子类只有默认构造函数(无参数,无名称)。

命名构造函数

使用命名构造函数为类实现多个构造函数去实现更加的清晰。

  class Point {
    num x, y;

    Point(this.x, this.y);

    // 命名构造函数

    Point.origin() {
      x=0;
      y=0;
    }
  }

记住构造函数是不能被继承的,就意味着超类(父类的)命名构造函数也不能被子类继承。如果希望使用超类中定义的命名构造函数创建子类,则必须在子类中实现该构造函数。

调用非默认的超类构造函数

默认的,在子类调用的超类构造函数是没有命名、没有参数的。超类构造函数在构造函数的开头被调用;如果一个初始化列表也被调用,它将在超类调用之前执行。总之,执行顺序如下:

  1. 初始化列表
  2. 超类无参构造函数
  3. 主类无参构造函数

如果超类没有未命名的无参构造函数,你必须手动在超类中调用一个构造函数。在冒号后面指定超类的构造函数,在构造函数体的前部。

在下面的示例中,Employee 类的构造函数调用它的超类 Person 的命名构造函数

  class Person {
    String firstName;

    Person.fromJson(Map data) {
      print('in Person');
    }
  }

  class Employee extends Person {
    // Person没有默认的构造函数
    // 你必须调用super.fromJson(data)
    Employee.fromJson(Map data) : super.fromJson(data) {
      print('in Employee');
    }
  }

  main() {
    var emp = new Employee.fromJson({});
  }

  // Prints:
  // in Person
  // in Employee

  if (emp is Person){
    // 类型检查
    emp.firstName = 'Bob';
  }
  (emp as Person).firstName = 'Bob';

因为在调用构造函数之前会处理超类构造函数的参数,所以参数可以是一个表达式,比如函数调用:

  class Employee extends Person {
    Employee() : super.fromJson(getDefaultData());
    // ...
  }

注意:超类构造函数的参数没有 this 的权限,例如,参数可以调用静态方法但是不能调用实例方法。

初始化列表

除了调用超类的构造函数外,你也可以在构造函数体运行之前,初始化实例变量。用逗号分隔初始化:

  // 初始化列表在构造函数体运行之前设置实例的变量

  Point.fromJson(Map<String, num> json)
    : x = json['x'],
      y = json['y'] {
        print('In Point.fromJson() : ($x, $y)');
      }

注意:初始化程序的右侧无权访问 this。

在开发期间,你可以在初始化列表验证输入的值

  Point.withAssert(this.x, this.y) : assert(x >= 0) {
    print('In Point.withAssert() : ($x, $y)');
  }

设置 final 字段时,设置初始化列表很方便。在下面的示例中在初始化列表中初始化了三个 final 字段。

  import 'dart:math';

  class Point {
    final num x;
    final num y;
    final num distanceFromOrigin;

    Point(x, y)
      : x = x,
        y = y,
        distanceFromOrigin = sqrt(x * x + y * y);
  }

  main() {
    var p = new Point(2, 3);
    print(p.distanceFromOrigin);
  }

重定向构造函数

有时候构造函数只用于在同一个类中重定向到其他的构造函数。重定向构造函数的函数体是空的,构造函数的调用出现在冒号之后。

  class Point {
    num x, y;

    // 这个类的主构造函数
    Point(this.x, this.y);

    // 代表主构造函数
    Point.alongXAxis(num x) : this(x, 0);
  }

常构造函数

如果你的类生成不打算更改的对象,你可以使这些对象为编译时常量。为了去实现,定义一个 const 构造函数,并保证所有的实例变量都是 final 型。

  class ImmutablePoint {
    static final ImmutablePoint origin  = const ImmutablePoint(0, 0);

    final num x, y;
    const ImmutablePoint(this.x, this.y);
  }

常构造函数并不总是创造常量。更多细节,去看 使用构造函数 那一节。

工厂构造函数

使用关键字 factory 当实现构造函数不总在它的类中创建新的实例。比如,工厂构造函数可能从缓存(cache)中返回实例,或者返回子类型实例。

下面的示例演示了一个构造函数从缓存中返回了一个对象:

  class Logger {
    final String name;
    bool mute = false;

    // _cache是库私有,因为标识符前面的_
    static final Map<String, Logger> _cache = <String, Logger>{};

    factory Logger(String name) {
      if (_cache.containsKey(name)) {
        return _cache[name];
      } else {
        final logger = Logger._internal(name);
        _cache[name] = logger;
        return logger;
      }
    }
  }

  Logger._internal(this.name);

  void log(String msg) {
    if(!mute) print(msg);
  }

工厂构造函数没有 this 的权限。

调用工厂构造函数跟调用其他的构造函数一样:

  var logger = Logger('UI');
  logger.log('Button clicked');

方法

方法是赋予对象动作的函数。

实例方法

在对象中的实例方法有使用实例对象和 this 的权限。下面例子中的 distanceTo() 方法就是一个实例方法的示例:

  import 'dart:math';

  class Point{
    num x, y;

    Point(this.x, this.y);

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

Getters 和 Setters

Getters 和 Setters 是提供了读写对象属性的特殊方法。回想一下,每一个实例变量都有一个隐性的 getter,如果合适的话还有一个 setter。你可以通过 get 和 set 关键字实现创建额外的属性。

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

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

    // 定义两个计算的属性:right和bottom
    num get right => left + width;
    set right(num value) => left = value - width;
    num get bottom => top + height;
    set bottom(num value) => top = value - height;
  }

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

通过 getters 和 setters,你从实例变量开始,稍后使用方法包装它们,无需更改客户端的代码。

注意:无论是否明确定义了 getter,如同 ++ 的运算符都会如于其那样工作。去避免任何不可预测的影响,运算发只需要调用一次 getter,并将其值保存在临时变量中。

抽象类

使用 abstract 修饰符定义抽象类——不能被实例化的类,抽象类对于定义接口是非常有用的,通常还有一些实现。如果你想使抽象类初始化,那就定义一个 factory(工厂)构造函数

抽象类一般有抽象方法,下面是一个带有抽象方法的抽象类的示例:

  // 这个类被定义为抽象类,不能被实例化
  abstract class AbstractContainer {
    // 定义构造函数,域,方法。。。

    void updateChildren(); // 抽象方法
  }

隐式接口

每个类都隐式定义了一个接口,接口包含所有该类的实例成员及其实现的接口。如果你想在不继承 B 类的情况下创建支持 B 类 API 的 A 类,A 类应该实现 B 接口。

类通过在 implements 子句中声明它们,然后提供接口所需的 API 来实现一个或者多个接口:

  // 一个Person,隐式接口包含greet()
  class Person {
    // 在界面中,仅能在这个库中可见
    final _name;

    // 因为是个构造函数,所以不在界面中
    Person(this._name);

    // 在界面中
    String greet(String who) => 'Hello, $who.I am &_name.'
  }

  // Person接口的实现
  class Impostor implements 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()));
  }

这是一个指定类实现多个接口的示例:

  class Point implements Comparable, Location {...}

扩展类(继承与派生)

使用 extends 创造子类,并且使用 super 指向超类:

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

  class SmartTelevision extends Television {
    void turnOn() {
      super.turnOn();
      _bootNetworkInterface();
      _initializeMemory();
      _upgradeApps();
    }
    // ...
  }

重载成员

子类可以重写实例方法,getters,setters。你可以使用 @override 注释来指示你有意重载的成员:

  class SmartTelevision extends Television {
    @override
    void turnOn() {...}
    // ...
  }

在代码中,去限制方法参数或者实例变量的类型是 类型安全 的,你可以使用 协变关键字 covariant keyword

重载运算符

你可以重载下表中的运算符。举个例子,如果你定义一个 Vector 类,你可以定义一个 + 方法相加两个 vector(译者注:可以参考 C++ 在这部分的内容)。

可重载运算符表
< + []
> / ^ []=
<= ~/ & ~
>= * << ==
- % >>

你可能注意到了!= 是不可以重载的运算符,表达式 e1 != e2 只是!(e1 == e2) 的句法糖(译者注:看到这里,译者觉得所谓句法糖的意思,貌似是等价写法的意思,嘤嘤嘤

下面是类重载运算符的一个例子:

  class Vector {
    final int x, y;

    Vector(this.x, this.y);

    Vector operator + (Vector v) => Vector(x + v.x, y + v.y);
    Vector operator - (Vector v) => Vector(x - v.x, y - v.y);

    // 运算符 == 和 哈希码并没有显示。更多的细节在后面的note中。
  }

  void main() {
    final v = Vector(2, 3);
    final w = Vector(2, 2);

    assert(v + w == Vector(4, 5));
    assert(v - w == Vector(0, 1));
  }

如果你重载了 == ,你也应该重载对象的哈希码 getter。举个例子重载 == 和哈希码,参见 实现地图键(在库之旅中)

更多关于重载的信息,通常参考 扩展类(继承)

没有这个方法 noSuchMethod()

要在代码中尝试使用不存在的方法或实例变量时,检测或做出反应,你可以重载 noSuchMethod():

  class A {
    // 除非你重载noSuchMethod,否则使用不存在的成员会导致NOSuchMethodError。

    @override
    void noSuchMethod(Invocation invocation) {
      print('You tried to use a non-existent member: ' + '${invocation.memberName}');
    }
  }

不可以 调用一个没有实现的方法,除非满足下面的任意一点:

  • 接收器具有静态的类型 dynamic

  • 接收器具有静态的类型定义了没有实现的方法(抽象是可以的),接收器的 dynamic 类型有 noSuchMethod() 的实现,跟类对象是不一样的

更多请参照 noSuchMethod 转发规范 noSuchMethod forwarding specification

枚举类型 Enumerated types

枚举类型,通常被称为 enumerations 或者 enums ,是被用来代表常量的固定数字的特殊一类。

使用枚举

使用关键字 enum 声明枚举类型:

  enum Color { red, green, blue }

每个枚举的值都有一个 索引 index getter,返回从零开始的索引。

  assert(Color.red.index == 0);
  assert(Color.green.index == 1);
  assert(Color.blue.index == 2);

去获取枚举中所有值的列表,你可以使用枚举的 values 常方法

  List<Color> colors = Color.values;
  assert(Color[2] == Color.blue);

你可以在 switch 语句使用枚举,如果你不处理枚举的所有的值你将会收到警告。

  var aColor = Color.blue;

  switch (aColor) {
    case Color.red:
      print('Red as roses!');
      break;
    case Color.green:
      print('Green as grass!');
      break;
    default: // 避免警告
      print(aColor);
  }

枚举类型有下面的限制:

  • 你不可以创建子类,混入,或者实现枚举
  • 你不可以明确地实例化枚举

了解更多,参见 Dart 语言规范

向类添加功能:混入 mixins

Mixins 是一种在多个类层次结构中重用类代码的方法。

使用 mixin,请使用 with 关键字后跟一个或多个 mixin 名称。以下示例显示了两个使用 mixins 的类:

  class Musician extends Performer with Musical {
    // ...
  }

  class Maestro extends Person with Musical, Aggressive, Demented {
    Maestro(String maestroName) {
      name = maestroName;
      canConduct = true;
    }
  }

去实现 mixin,创建一个类并扩展对象、不要声明构造函数。除非您希望 mixin 可用作常规类,否则请使用 mixin 关键字而不是 class。

  mixin Musical {
    bool canPlayPiano = false;
    bool canCompose = false;
    bool canConduct = false;

    void enterainMe() {
      if (canPlayPiano) {
        print('Playing Piano');
      } else if(canConduct) {
        print('Waving hands');
      } else {
        print('Humming to self');
      }
    }
  }

可以使用 mixin 指定一个明确的类型,举个例子,你的 mixin 可以调用一个没有定义的方法——使用 on 指向调用的超类。

  mixin MusicalPerformer on Musician {
    // ...
  }

版本提醒:在 Dart2.1 中介绍了对于 mixin 的支持。早期版本的代码通常使用抽象类代替。更多关于 2.1 中 mixin 变化的信息,参见 Dart SDK 改变日志 Dart SDK changelog2.1 mixin 规范 2.1 mixin specification

类变量和方法

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

静态变量

静态变量(类变量)对类范围的状态和常量是非常实用的:

  class Queue {
    static const initialCapacity = 16;
    //...
  }

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

静态变量直到使用的时候才能被初始化。

此页遵循样式指南建议更偏向为常名称使用 lowerCamelCase。

静态方法

静态方法(类方法)不在实例中作用,因此无 this 权限。

  import 'dart:math';

  class Point {
    num x, y;
    Point(this.x, this.y);

    static num distanceBetween(Point s, Point b) {
      var dx = a.x - b.x;
      var dy = a.y - a.y;
      return sqrt(dx * dx + dy * dy);
    }
  }

  void main() {
    var a = Point(2, 2);
    var b = Point(4, 4);
    var distance = Point.distanceBetween(a, b);
    assert(2.8 < distance && distance < 2.9);
    print(distance);
  }

对于常用或广泛使用的实用程序和功能,请考虑使用顶级函数而不是静态方法。

你可以使用静态方法作为编译时常量。举个例子,可以将静态方法作为参数传递给常量构造函数。

泛型 Generics

如果你查看基础 API 文档数组,列表,你将会看到类型其实是 List。<…> 将列表标记为泛型(或者参数化)类型——具有正式参数类型的参数。按照惯例,类型变量有单个字母名称,比如 E,T,S,K 和 V。

为什么要使用泛型?

译者注:泛型即为模板

泛型常被用于类型安全,然而它们有比仅仅让你的代码运行的更多好处。

  • 正确指定泛型类型可以生成更好的代码。
  • 您可以使用泛型来减少代码重复。

如果你希望列表只包含字符串,你可以声明它为 List(读作“字符串列表”)。这样,你,你的程序和你的工具检测列表中的非字符串参数是个错误。

  var names = List<String>();
  names.addAll(['Seth', 'Kathy', 'Lars']);
  names.add(42); // 错误

使用泛型的另一个原因是减少代码重复。泛型允许您在多种类型之间共享单个接口和实现,同时仍然利用静态分析。比如创建一个缓存对象的接口

  abstract class ObjectCache {
    Object getByKey(String key);
    void setByKey(String Key, Object value);
  }

你发现你想创建一个指定字符串版本的接口,你创建另一个接口:

  abstract class StringCache {
    String getByKey(String key);
    void setByKey(String Key, String value);
  }

然后你又想创建一个指定数字版本的接口。。。

泛型类型可以解决你创建所有的这些接口的问题。取而代之,你可以带有类型参数创建单个接口:

  abstract class Cache<T> {
    T getByKey(String key);
    void setByKey(String key, T value);
  }

在这段代码中,T 是替身类型。它是一个占位符,您可以将其视为开发人员稍后定义的类型。

使用集合文字

列表和集合文字可以被参数化。参数化文字除了在括号之前添加类型,其他就像你之前看到的那样。

  var names = <String>['Seth', 'Kathy', 'Lars'];
  var pages = <String, String>{
    'index.html': 'Homepage',
    'robot.txt': 'Hints for web robots',
    'humans.txt': 'We are people, not machines'
  };

使用带有构造函数的参数化类型

在使用构造函数的时候,指定一个或者多个类型,将类型放在类名后面的尖角括号之中

  var names = List<String>();
  names.addAll(['Seth', 'Kathy', 'Lars']);
  var nameSet = Set<String>.from(names);

下面的代码创建了一个整型键、View 类型的地图

  var views = Map<int, View>();

泛型集合和它们包含的类型

Dart 泛型的类型具体化,意味着它们在运行时携带类型信息。

  var names = List<String>();
  names.addAll(['Seth', 'Kathy', 'Lars']);
  print(names is List<String>); // 输出true

相反,在 Java 中泛型擦除,就意味着在运行中删除泛型类型参数。在 Java 之中,你可以测试对象是否 List,但是不能测试它是否是 List。

限制参数化类型

当你实现泛型类型之时,你需要去限制参数的类型。你可以使用 extends。

  class Foo<T extends SomeBaseClass> {
    String toString() => "Instance of 'Foo<$T>'";
  }

  class Extender extends SomeBaseClass {...}

可以使用 SomeBaseClass 或其任何子类作为泛型参数:

  var someBaseClassFoo = Foo<SomeBaseClass>();
  var extenderFoo = Foo<Extender>();

也可以不指定泛型参数:

  var foo = Foo();
  print(foo); // Instance of 'Foo<SomeBaseClass>'

指定非 SomeBaseClass 会导致错误:

  var foo = Foo<Object>(); // 错误

使用泛型方法

起初,Dart 的泛型支持被限制在类。一个更新的语法,叫做泛型方法,允许类型参数在方法或者函数中:

  T first<T>(List<T> ts) {
    // 先做一些初始化工作或者错误检查
    T tmp = ts[0];
    //
  }

这里在 first中泛型类型参数,允许你在一些地方使用类型参数 T

  • 在函数的返回类型 (T)
  • 在参数类型 (List)
  • 在局部变量 (T tmp)

了解关于泛型的更多信息,参考 使用泛型方法

库与能见度

import 和 library 指令可以帮助你创建一个模块化的、可分享的代码库。库不仅仅提供 API,还有隐私单元:以下划线开头的标识符只在库内可见。每一个 Dart APP 都是一个库,即使它没有使用 library 指令。

可以使用包来分发库。在发布包和内容管理中了解关于 pub 有关的信息,在 SDK 中包含包管理。

使用库

使用 import 指定如何从一个库中的命名空间在另一个库的范围内使用。

举个例子,Dart web 应用通常使用 dart:html库。

  import 'dart:html';

import 需要的唯一参数是指定库的 URI。对于内置的库,URI 有特殊的 dart: 方案。对于其他的库,你可以使用文件系统路径或者 package: 方案。package 方案指定由类似于 pub tool 的包管理提供的库。

  import 'package:test/test.dart';

URI 代表了统一资源标识符。URL(uniform resource locators)(统一资源定位符) 是一种常见的 URI。

指定一个库前缀

如果你输入两个含有冲突的标识符的库,然后你可以为一个或者所有的库指定前缀。举个例子,如果库 1 和库 2 都有 Element 类:

  import 'package:lib1/lib1.dart';
  import 'package:lib2/lib2.dart' as lib2;

  // 使用来自于库1的Element
  Element element1 = Element();

  // 使用来自于库2的Element
  lib2.Element element2 = lib2.Element();

输入库的一部分

如果你只想使用库的一部分,你可以有选择性的输入库。

  // 只输入foo
  import 'package:lib1/lib.dart' show foo;

  // 输入除了foo
  import 'package:lib2/lib2.dart' hide foo;

懒加载一个库

延期加载(或者称为懒加载) 允许应用在有需要的时候加载一个库。以下是您可能使用延迟加载的一些情况:

  • 减少应用程序的初次启动时间
  • 执行 A/B 测试——尝试算法的替代实现
  • 加载很少使用的功能,例如可选的屏幕和对话框

要懒加载一个库,你必须首先使用 deferred as。

  import 'package:greetings/hello.dart' deferred as hello;

当你使用库的使用,使用库的标识符调用 loadLibrary()

  Future greet async{
    await hello.loadLibrary();
    hello.printGreeting();
  }

在前面的代码中,await 关键字暂停了执行,直到库被加载。了解关于 async 和 await,参考 异步支持 asynchrony support

你可以在一个库中调用 loadLibrary() 很多次,没有任何问题,库只被加载一次。

使用延时加载的时候,记住以下内容:

  • 延迟库的常量不是导入文件中的常量。在加载延迟库之前,这些常量不存在。
  • 不能在导入文件中使用延迟库中的类型,考虑将接口类型移动到由延迟库和导入文件导入的库。
  • 你使用 deferred as namespace,Dart 将 loadLibrary()隐式插入到命名空间。loadLibrary() 函数返回一个 Future。

DartVM 区别:即使在调用之前,Dart VM 也允许访问延迟库的成员 loadLibrary()。此行为可能会更改,因此 不要依赖于当前的 VM 行为

详细信息参见 issue #33118

实现库

参阅 创建库包 需求如何去实现一个库包,包括:

  • 如何组织库源代码
  • 如何使用 export 指令
  • 何时使用 part 指令

异步支持

Dart 库中包含很多返回 Future 或者 Stream对象的函数。这些函数是异步的。它们在设置可能耗时的操作后(比如 I/O)返回,而不等待操作完成。

async 和 await 关键字支持异步编程,让你写异步代码看起来更像是同步代码。

处理 Futures

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

  • 使用 async 和 await
  • 使用 Future API,在 库之旅 中有所描述。

代码使用 async 和 await 是异步的,但是它看起来很像是同步代码。举个例子,这有一些使用 await 关键字等待异步函数结果的代码:

  await lookUpVersion();

去使用 await,代码必须是一个异步函数——一个函数被标记为 async:

  Future checkVersion() async {
    var version = await lookUpVersion();
  }

注意:虽然异步函数可能执行耗时的操作,但它不会等待这些操作。异步函数只有在遇到第一个 await 表达式(详情)时才会执行等待。然后它返回一个 Future 对象,仅在 await 表达式完成后才恢复原样执行。

详情

使用 try,catch,finally 去处理和清除使用 await 代码的错误:

  try {
    version = await lookUpVersion();
  } catch(e) {
    // 无法查找版本
  }

你可以在异步函数中多次使用 await。举个例子,下面的代码等待了三次函数结果:

  var entrypoint = await findEntryPoint();
  var exitCode = await runExecutable(entrypoint, args);
  await flushThenExit(exitCode);

在 await 表达式中,通常结果都是 Future;如果不是,那么值自动包含在了 Future 中。这个 Future 对象表示返回一个对象的承诺,await 表达式的值是返回一个对象。await 表达式使执行暂停直到对象可用。

如果使用 await 的时候出现了编译时错误,请确保 await 是在一个异步函数之中 (async function)。 举个例子,在 app 中的 main()函数中使用 await,main() 的函数体必须被标记为 async:

  Future main() async {
    checkVersion();
    print('In main: version is ${await lookUpVersion()}');
  }

声明异步函数

异步函数是通过标记 async 修改函数体的函数。

将 async 关键字加到函数上,使它返回一个 Future。举个例子,考虑一个返回 String 的同步函数:

  String lookUpVersion() => '1.0.0';

如果将它改为异步函数——举个例子,future 表现执行起来会非常耗时——返回值是一个 Future。

  Future<String> lookUpVersion() async => '1.0.0';

注意这个函数体不需要使用 Future 的 API。在必要的情况下,Dart 创造一个 Future 对象。

如果你的函数不返回任何有用的值,让它的返回类型为 Future。

处理流 (Streams)

如果你想从流中获取值,你有两种方法:

  • 使用 async 和一个异步 for 循环 (await for)。
  • 使用 Stream API,在 库之旅 中有阐述。

注意:在使用 await for 之前,请确保它使代码更清晰并且你确实想去等待流的所有的值。例如,你通常不应该为 UI 事件侦听器使用 await for,因为 UI 框架发送无穷尽的事件流。

异步 for 循环有以下的形式:

  await for (var或者类型 标识符 in 表达式) {
    // 在每一次流发射值的时候执行
  }

表达式的值必须有流类型。执行过程如下:

  1. 等待直到流发射一个值
  2. 执行函数体中的 for 循环,变量值为被射出的值
  3. 重复 1 和 2 直到流被关闭

停止对流的监听,你可以使用一个 break 或者 return 语句,退出 for 循环和从流中退订。

如果在表现异步 for 循环的时候出现编译时错误,确保 await for 在 asyn 函数内。 举个例子,你在你的 app 的 main()函数使用异步 for 循环,main() 的函数体必须被标记为 async。

  Future main() async {
    // ...
    await for (var request in requestServer) {
      handleRequest(requset);
    }
    // ...
  }

获取更多关于异步编程的信息,通常上,在库之旅中参考dart:async章节。

也可以看文章Dart 异步支持:1Dart 异步支持:2Dart 语言指向

发生器 Generators

当你需要懒生成一连串的值时,考虑使用发生器函数。Dart 内置两种发生器函数:

  • 同步发生器:返回一个可迭代对象
  • 异步发生器:返回一个对象

表现一个同步发生器,用 sync* 标记函数体,用 yield 传递值:

  Iterable<int> naturalsTo(int n) sync* {
    int k = 0;
    while (k < n) yield k++;
  }

表现一个异步发生器,用 async* 标记函数体,用 yield 传递值:

  Stream<int> asynchronousNaturalsTo(int n) async* {
    int k = 0;
    while (k < n) yield k++;
  }

如果你的生成器是递归,你可以使用 yield* 提高其性能:

  Iterable<int> naturalsDownFrom(int n) sync* {
    if (n > 0){
      yield n;
      yield* naturalsDownFrom(n - 1);
    }
  }

了解更多关于发生器,参考文章Dart 异步支持:2

可调用的类

允许你的类像函数一样被调用,使用 call() 方法。

在下面的示例中,WannabeFunction 类定义了一个 call() 函数,接受三个字符串并连接它们,用空格分隔每个字符串,并附加一个感叹号。

  class WannabeFunction {
    call(String a, String b, String c) => '$a $b $c!';
  }
  main() {
    var wf = WannabeFunction();
    var out = wf("Hi", "there", "gang");
    print('$out');
  }

了解更多关于将类如同函数一样对待的信息,请参考在 Dart 中模拟函数

分离 Isolates

大多的计算机,甚至一些移动设备上,都有多核 CPU。为了利用所有的核心,开发人员传统上使用并发运行的共享内存线程。但是,共享状态并发容易出错,并且可能导致代码复杂化。所有 Dart 代码都在隔离区内运行,而不是线程。每个隔离区都有自己的内存堆,确保不会从任何其他隔离区访问隔离区的状态。有关更多信息,请参阅 dart:isolate 库文档

类型定义 Typedefs

在 Dart 中,函数是对象,就如同字符串和数字是对象那样。一个类型定义,或者一个功能型的别名,给予了函数一个名字,你可以在声明的时候和返回类型的时候使用它。当函数类型分配给变量时,typedef 会保留类型信息。

下面是没有使用 typedef 的代码:

  class SortedCollection {
    Function compare;

    SortedCollection(int f(Object a, Object b)) {
      compare = f;
    }
  }

  // 初始化

  int sort(Object a, Object b) => 0;

  void main() {
    SortedCollection coll = SortedCollection(sort);

    // 我们都知道compare是一个函数,但是什么类型的函数呢?

    assert(coll.compare is Function);
  }

当把 f 的值赋给 compare 时,类型信息就丢失了。f 的类型是 (Object, Object) -> int(其中 -> 表示返回),但是 compare 的类型是 Function。如果我们将代码更改为使用显式名称并保留类型信息,则开发人员和工具都可以使用该信息。

  typedef Compare = int Function(Object a, Object b);

  class SortedCollection {
    Compare compare;

    SortedCollection(this.compare);
  }

  // 初始化

  int sort(Object a, Object b) => 0;

  void main() {
    SortedCollection coll = SortedCollection(sort);
    assert(coll.compare is Function);
    assert(coll.compare is Compare);
  }

目前,typedef 仅限于函数类型,开发者希望改变这一点。

因为类型定义只是别名,它们提供了检查函数类型的方法。

  typedef Compare<T> = int Function(T a, T b);

  int sort(int a, int b) => a - b;
  void main() {
    assert(sort is Compare<int>);       // True
  }

元数据 Metadata

使用元数据给关于你的代码额外的信息。元数据注释以字符 @开头,后跟对编译时常量(如 deprecated)的引用或对常量构造函数的调用。

所有的 Dart 代码都有两个注释:@deprecated 和 @override,有关使用 @override 的示例,参阅扩展类,以下是使用 @deprecated 的示例:

  class Television {
    /// _Deprecated:使用[turnOn]代替_
    @deprecated
    void actvate() {
      turnOn();
    }

    /// 打开TV的电源
    void turnOn(){...}
  }

你可以定义你自己的元数据注释。下面的例子中定义了一个 @todo 注释并带有两个参数。

  library todo;
  class Todo {
    final String who;
    final String what;

    const Todo(this.who, this.what);
  }

下面是使用 @todo 的一个示例:

  import 'todo.dart';

  @Todo('seth', 'make this do something')
  void doSomething() {
    print('do something');
  }

元数据可以在库、类、类型定义、类型参数、工厂、字段、参数、或变量声明以及导入或导出指令之前出现,可以使用反射在运行时检索元数据。

评论(代码注释)

Dart 支持单行、多行、文档评论。

单行(以 "//" 开头,忽略后面内容)

多行(介于 /* */ 之间,可以嵌套)

文档

文档评论是以 /// 或者 /** 开头的多行或者单行评论,在连续的行使用 /// 的效果与多行评论的效果相同。

在文档评论中,Dart 编译器忽略所有的文本,除非它在括号之中。使用括号,可以引用类、方法、字段、顶级变量、函数和参数。括号中的名称在已记录的程序元素的词法范围内得到解析。

以下是文档注释的示例,包含对其他的类和参数的引用:

  /// A domesticated South American camelid (Lama glama).
  ///
  /// Andean cultures have used llamas as meat and pack
  /// animals since pre-Hispanic times.
  class Llama {
    String name;

    /// Feeds your llama [Food].
    ///
    /// The typical llama eats one bale of hay per week.
    void feed(Food food) {
      // ...
    }

    /// Exercises your llama with an [activity] for
    /// [timeLimit] minutes.
    void exercise(Activity activity, int timeLimit) {
      // ...
    }
  }

在生成的文档之中,[Food] 成为了 Food 类 API 文档的链接。

要解析并生成 HTML 文档,你可以使用 SDK 的 文档生成工具 。有关生成文档的示例,参阅 Dart API 文档 。有关如何构建评论的建议,请参阅 Dart Doc 评论指南

总结

本教程概述了 Dart 语言中的常用功能。正在实施更多功能,设计者不希望破坏现有的代码体系。更多信息参阅 Dart 语言规范有效 Dart

学习更多 Dart 的核心库,参见 Dart 库之旅

0 打赏
打赏 1 积分后可见