Dart语言速览

我在之前的一篇Flutter安装教程中提到了Flutter采用的语言是Dart,所以要学习Flutter必须要学习Dart。这语言非常新,由Chrome在2011年10月份推出第一版,目前生态也相对较小,所以Dart的存在感一直比较低,但我相信Flutter会越来越火热,必将反过来推动Dart生态的发展!在大致学了一遍Dart的语法后,我发现它很像是JavaScript和Java的结合体(TypeScript?😂):Dart是一门面向对象的强类型语言,拥有可运行的class、元数据(注解)、泛型、抽象、反射、异步支持...等各种机制。接下来我将列出Dart(version: 2.2.0)的部分语法糖(异常处理、流程控制语句等和C、JS等都几乎一致,所以不再介绍)。

注:如果你想跑本文中的示例,需要安装Dart SDK,本文所用Dart版本为2.1.0

Dart 语法列表

语法概述[1]

新建一个demo.dart,写入一个基本的dart程序:

// 这是程序执行的入口。
main() {
  add(1, 2);
}
add(firstNum, secondNum) {
  num result = firstNum + secondNum;
  print('$firstNum + $secondNum = $result');
}

可以在终端输入dart demo.dart执行程序。

  1. main函数:这是所有Dart程序执行的入口,有且仅有一个,和C语言中的main一样;
  2. add方法:一个普通的接收两个形参的函数;
  3. num:定义result的类型为数字类型,如果确定该数字类型是浮点型或者是整型,也可以更具体地指定为doubleint
  4. print:用来打印内容的内置方法;
  5. $vars:字符串插值,在字符串字面量中引用变量或者表达式;
  • 所有能够使用变量引用的都是对象,每个对象都是一个类的实例。所有的对象都继承于 Object 类。
  • 和Java 不同的是,Dart 没有 public、 protected、private 关键字。如果一个标识符以 _ 开头,则该标志符在库内是私有的。

关键字列表[2]

keywords.png
  1. 带有上标 1 的关键字是内置关键字。避免把内置关键字当做标识符使用。 也不要把内置关键字用作类名字和类型名字。
  2. 带有上标 2 的关键字,是在Dart 1.0发布以后又新加的,用于支持异步相关的特性。 你不能在标记为 asyncasync*、或者 sync* 的方法体内使用 asyncawait、或者 yield 作为标识符。

变量[3]

1. 变量声明及初始化

一个简单的变量声明如下:

var name = '张三';

其中var关键字表示接收任意类型的变量,但是和JavaScript不同的是,Dart中的var一旦接收了一次赋值,其声明的变量类型就确定下来了,不能再更改,且在同作用域中不能再次声明。
如下所示的代码在Dart中会报错:

testVariable() {
  var name = '张三';
  name = 22;
  print(name);
}
// 执行将报错:Error: A value of type 'dart.core::int' can't be assigned to a variable of type 'dart.core::String'.
testVariable() {
  var name = '张三';
  var name = '李四';
  print(name);
}
// 执行将报错:Error: 'name' is already declared in this scope.

2. 默认值:没有初始化的变量自动获取一个默认值为null

int a;
print(a == null); // true

3. final & const

  • 使用final或const定义的变量不能被更改:
final int a = 2;
a = 3; // 报错:'a', a final variable, can only be set once.
const int b = 4;
b = 5; // 报错:Constant variables can't be assigned a value.
  • final 或者 const 都不能和 var 同时使用。
  • final和const不同在于,const变量为编译时常量,需要在编译期间就确定值,const声明必须用const类型的值初始化。如:
int a = 2;
final int b = a * 3; // 6
const int c = a * 3; // 报错:Const variables must be initialized with a constant value.
  • const 变量同时也是final变量。
  • 如果 const 变量在类中,必须定义为 static const。static 表示成员在类本身上可用,而不是在类的实例上。如:
class TestConst {
  const int a = 3; // 报错:Only static fields can be declared as const.
  static const int b = 3; // 正确写法
}
  • const 关键字不仅仅只用来定义常量,还可以用来创建不变的值,还能定义const类型的构造函数,这种类型的构造函数创建的对象是不可改变的。任何变量都可以有一个不变的值:
testConst() {
  List foo = const [2, 4];
  foo.add(6); // 报错:Unsupported operation: Cannot add to an unmodifiable list
  print(foo);
}

内置类型[4]

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

  • numbers
  • strings
  • booleans
  • lists
  • maps
  • runes
  • symbols

1. numbers
如前面所说,Dart一共可以用四种方式声明一个数字变量:var,num,int,double。
其中num是int和double的父类,用来声明不确定浮点型还是整型的数字,且变量可以重新赋值浮点或整型的变量;而当用var声明数字变量时,初始化时就会确定其为整型还是浮点型,若为整型,则不能重新赋值一个浮点数字了,若为浮点型,再重新赋值整型数字时,会自动转换成浮点型。如下示例一目了然:

var a = 2;
a = 3.14; 
// 报错:A value of type 'double' can't be assigned to a variable of type 'int'.

而下面这样是可以的,但是a输出为浮点型:

var a = 3.14;
a = 2;
print(a) ; // 2.0

double和int声明的情况和var一致,只是var初始化时可选择类型,double和int初始化时就必须明确:

int a = 3.14;
// 报错:A value of type 'double' can't be assigned to a variable of type 'int'.
double b = 2;
print(b); // 2.0

2. strings

  • 可以在字符串中使用表达式,用法是这样的: ${expression}。如果表达式是一个比赛服,可以省略 {}。 如果表达式的结果为一个对象,则 Dart 会调用对象的 toString() 函数来获取一个字符串。
String name = 'Tony';
List<String> subjects = ['math', 'physics'];
print('$name is boy');
// Tony is boy
print('$name is good at ${subjects.map((item) => item.toUpperCase())}');
// Tony is good at (MATH, PHYSICS)
  • 可以使用 + 操作符来把多个字符串链接为一个,也可以把多个字符串放到一起来实现同样的功能:
String run = 'Tony ' + 'is ' + 'running';
String eat = 'Tony ' 'is ' 'eating'; 
print(run); // Tony is running
print(eat); // Tony is eating
  • 使用三个单引号或者双引号也可以创建多行字符串对象:
  String work = '''
  Tony is working in the office;
  He is tired;
  He needs a good sleep;
  ''';
  print(work);

/*
  Tony is working in the office;
  He is tired;
  He needs a good sleep;
*/

3.booleans
Dart在需要一个布尔值的时候,只有值为true的变量才被认为是true,其他所有情况都被认为是false,这一点和JavaScript是完全不同的,如以下代码,如果是JavaScript解释器执行的话将输出:how are u! 而在Dart中将会报错:

var name = 'Tony';
if (name) { // 报错:Conditions must have a static type of 'bool'.
  print('how are u!');
} else {
  print('anybody here?');
}

4. lists
Dart中的list可被理解为JS中的Array,两种数据结构几乎一模一样,只是用法上有所不同。

  testList() {
  var list = [];
  print(list.isEmpty); // true

  list.add(3);
  list.addAll([4,6,8]);
  print(list); // [3, 4, 6, 8]

  var list2 = new List(4);
  print(list2); // [null, null, null, null]

  List list3 = [1,2,3];
  print(list3.map((item) => item * 2)); // (2, 4, 6)

  List<int> list4 = [5,2,7,9,1];
  list4.sort();
  print(list4); // [1, 2, 5, 7, 9]
}

更多的list方法可参考https://api.dartlang.org/stable/dart-core/List-class.html

5. maps
Dart中的Map和ES6中的Map类似,实例方法有所不同,下面是简单的一些例子:

testMap() {
  var cities = new Map();
  var list = ['NanJin', 'SuZhou'];
  cities['HangZhou'] = 'ZheJiang';
  cities['ChengDu'] = 'SiChuan';
  cities.addAll({
    'XiaMen': 'FuJian',
    'GuiYang': 'GuiZhou'
  });
  cities[list] = 'JiangSu';
  print(cities); // {HangZhou: ZheJiang, XiaMen: FuJian, GuiYang: GuiZhou, [NanJin, SuZhou]: JiangSu}
  print(cities['HangZhou']); // ZheJiang
  print(cities.length); // 4
  cities.clear();
  print(cities.isEmpty); // true
}

6. runes
这玩意儿不常用。在 Dart 中,runes 代表字符串的 UTF-32 code points,而普通的Dart字符串是UTF-16 code units序列,所以在Dart中,使用\uXXXX的形式表示32-bit Unicode,其中XXXX是4个16进制的数,如果是非4个数值的情况,把编码值放到大括号中即可。 例如, emoji表情 (😀) 是 \u{1f600}。
String 类有一些属性可以提取 rune 信息。 codeUnitAtcodeUnit 属性返回 16-bit code units。 使用 runes 属性来获取字符串的 runes 信息。

testUnicode() {
  var smile = '\u{1f600}';
  print(smile);
  Runes emoji = new Runes('\u2665 \u{1f605} \u{1f60e} \u{1f47b} \u{1f596} \u{1f44d}');
  print(new String.fromCharCodes(emoji));
}

输出结果为:


Unicode.png

7. symbols
这玩意儿更不常用,甚至可能永远都用不上。做JS的人可能熟悉ES6中的symbol,但是这两者却是大大滴不同。ES6中的symbol用来定义一个独一无二的值,作为替代易混淆的字符串的一种标识。而在Dart中,symbol用来反射库中的元数据,这里需要超前接触一些概念了,比如class、库、反射等;但现在不做详细研究,大概知道用法和用途就好。

反射是在运行时获取某类元数据的一种机制,比如类中的方法数量、该类拥有的构造函数数量或函数中的参数数量。

新建Animal.dart,输入以下内容:

library animal_lib;   

class Animal {         
  walk() {        
    print('animals can walk'); 
  } 
  sleep() { 
    print('animals need to sleep'); 
  } 
  eat() { 
    print('animals need to eat'); 
  } 
}

class Person extends Animal{
  work() {
    print('a person need to work for money');
  }
}

新建AnimalSymbol.dart,输入以下内容:

import 'dart:core'; 
import 'dart:mirrors'; 
import 'Animal.dart';  

main() { 
  Symbol lib = new Symbol('animal_lib');   
  Symbol className = new Symbol('Animal');  
  // 像是上面的编译时常量也可以写成:Symbol className = #Animal;
  // print(#s == new Symbol('s')); //true
  testSymbolReflect(lib, className);
}

void testSymbolReflect(Symbol lib, Symbol className) { 
  MirrorSystem mirrorSystem = currentMirrorSystem(); 
  LibraryMirror libMirror = mirrorSystem.findLibrary(lib); 
  
  if (libMirror != null) { 
    print('there are ${libMirror.declarations.length} classes found in the library:'); 
    // 获取库中所有的类
    libMirror.declarations.forEach((s, d) => print(s));  
    // containsKey用来判断库中是否存在某一个类
    if (libMirror.declarations.containsKey(className)) print('found class');
    // 获取一个指定类的反射
    ClassMirror classMirror = libMirror.declarations[className]; 
    print('there are ${classMirror.instanceMembers.length} instance methods in the ${MirrorSystem.getName(className)}: ');
    // 获取该类中所有的实例方法
    classMirror.instanceMembers.forEach((s, v) => print(MirrorSystem.getName(s))); 
  }
}

执行dart AnimalSymbol.dart,期望输出结果:

symbol.png

操作符[5]

这里仅对一些比较有意思的操作符进行举例,很多操作符是大部分语言共通的,就不再介绍了。

  • ?.:条件成员访问,避免出现类似JS中Cannot read property 'name' of undefined的异常。
var a;
return a?.name; // 如过不用 ?. 而使用 . 会抛出异常
  • ??:是否为null,类似JS中的||
  var a;
  var b = a ?? 4;
  var c = b ?? 6;
  print(b); // 4
  print(c); // 4
  • ??=:是否为null,如果是则赋值
  var a = 2;
  var b = 6;
  var c;
  b ??= a;
  c ??= a;
  print(b); // 6
  print(c); // 2
  • ..:级联操作符,可以在同一个对象上连续调用多个函数以及访问成员变量。 使用级联操作符可以避免创建临时变量。
main() {
  var animal = new Animal();
  animal
    ..walk()
    ..sleep()
    ..eat();
}
class Animal {         
  walk() {        
    print('animals can walk'); 
  } 
  sleep() { 
    print('animals need to sleep'); 
  } 
  eat() { 
    print('animals need to eat'); 
  } 
}

  • ~/:除号,但是返回值为整数
  var a = 25;
  print(a ~/ 6); // 4
  • as:把对象转换为特定的类型。 一般情况下,你可以把它当做用 is 判定类型然后调用所判定对象的函数的缩写形式。例如下面的 示例:
if (emp is Person) { // Type check
  emp.firstName = 'Bob';
}

使用as简化上述操作:

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

注意: 上面这两个代码效果是有区别的。如果 emp 是 null 或者不是 Person 类型, 则第一个示例使用 is 则不会执行条件里面的代码,而第二个情况使用 as 则会抛出一个异常。

  • is:如果对象是指定的类型返回 true
  • is!:如果对象是指定的类型返回 false
  int a = 2;
  print(a is int); // true
  print(a is! int); // false

function(方法)[6]

1. 基础用法
和JS不一样,Dart中没有关键字function声明,且可以指定方法的返回类型,当然也可以不用指定。如下定义一个返回布尔类型的函数:

bool isEvenNumber(int number) {
  return number % 2 == 0;
}

对于只有一个表达式的方法,可以像ES6中的胖箭头那样简写,如上所示代码可以简写为:

bool isEvenNumber(int number) => number % 2 == 0;

方法可以有两种类型的参数:必需的和可选的。 必需的参数在参数列表前面, 后面是可选参数;可选参数可以是命名参数或者基于位置的参数,但是这两种参数不能同时当做可选参数。举个栗子:

  • 可选命名参数:需要把可选命名的参数放在{}中,调用的时候使用param: value的形式传入:
main() {
  print(add(n1: 2, n2: 9, n3: 4)); // 15
}
int add({int n3, int n2, int n1}) { // {}内参数顺序可以变
  return n1 + n2 + n3;
}
  • 可选位置参数:需要把可选位置的参数放在[]中:
main() {
  print(add(2, 9)); // 11
  print(add(2, 9, 4)); // 15
}
int add(int n1, int n2, [int n3]) {
  int sum = n1 + n2;
  if (n3 != null) {
    sum += n3;
  }
  return sum;
}
  • 默认参数值:默认参数值仅适用于可选参数,且默认值只能是编译时常量,如果没有提供默认值,则默认值为 null:
main() {
  print(add(2, 9)); // 15
}
int add(int n1, int n2, [int n3 = 4]) { // 默认n3的值为4
  int sum = n1 + n2;
  if (n3 != null) {
    sum += n3;
  }
  return sum;
}

2.匿名函数
和JS差不多的使用方法:

var list = ['apples', 'oranges', 'grapes', 'bananas', 'plums'];
list.forEach((i) { // forEach中就是一个匿名方法
  print(list.indexOf(i).toString() + ': ' + i);
});

3.函数是一等公民
一等公民指的是,函数可以像任何变量一样,赋值给另一个变量,或者作为参数传给另一个函数,或者作为返回值被一个函数return,它可以去任何值可以去的地方。举个栗子:

main() {
  testFunction()(3, 7); // 10
}
testFunction() {
  var foo = (int n1, int n2) {
    return n1 + n2;
  };
  return foo;
}

class(类)[7]

Dart 是一个面向对象编程语言,同时支持基于 mixin 的继承机制。每个对象都是一个类的实例,所有的类都继承于 Object。 基于 Mixin 的继承意味着每个类(Object 除外)都只有一个超类,一个类的代码可以在其他多个类继承中重复使用。
一个普通的类:

main() {
  var ins = new TestClass(); // 获取实例
  print(ins.a); // 9;
  print(ins.add()); // 11
  // 使用 ?. 来替代 . 可以避免当左边对象为 null 时候 抛出异常:
  var ins2;
  print(ins2?.add()); // null
}
class TestClass {
  num a = 9;
  num b = 2;

  add() {
    return a + b;
  }
}

1. 构造函数
定义一个和类名字一样的方法就定义了一个构造函数,类似于ES6中Class内部的constructor:

main() {
  var ins = new TestClass(6, 7);
  print(ins.a); // 6;
  print(ins.add()); //13
}
class TestClass {
  num a = 9;
  num b = 2;
  // 定义构造函数
  TestClass(int a, int b) {
    this.a = a;
    this.b = b;
  }
  // 上面的构造函数参数赋值给实例变量的操作可以简写成以下形式:
  // TestClass(this.a, this.b);
  add() {
    return this.a + this.b;
  }
}

2.命名构造函数
使用命名构造函数可以为一个类实现多个构造函数:

main(){
  var ins = new TestClass.doMath(6, 7);
  var ins2 = new TestClass.doPrint(); // I am printing something
  print(ins.add()); // 13
}
class TestClass {
  num a;
  num b;
  // 命名的构造函数
  TestClass.doMath(this.a, this.b);
  TestClass.doPrint(){
    print('I am printing something');
  }
  add() {
    return this.a + this.b;
  }
}

注意以下几点:

  • 如果你没有定义构造函数,则会有个默认构造函数。 默认构造函数没有参数,并且会调用超类的无名无参数的构造函数。
  • 构造函数不能继承。 子类如果没有定义构造函数,则只有一个默认构造函数 (没有名字没有参数)。
  • 如果超类没有无名无参数构造函数, 则你需要手工地调用超类的其他构造函数。 在构造函数参数后使用冒号 (:) 可以调用 超类构造函数。
main() {
  var ins = new TestClass();
  print(ins.add()); 
  //输出:
  // parent construtor
  // 7
}

class ParentClass {
  ParentClass() {
    print('parent construtor');
  }
}

class TestClass extends ParentClass {
  num a = 3;
  num b = 4;
  add() {
    return this.a + this.b;
  }
}

main() {
  var ins = new TestClass();
  print(ins.add()); 
  //输出:
  // parent construtor
  // child constructor
  // 7
}

class ParentClass {
  ParentClass.doPrint() {
    print('parent construtor');
  }
}

class TestClass extends ParentClass {
  num a = 3;
  num b = 4;
  TestClass(): super.doPrint() {
    print('child constructor');
  }
  add() {
    return this.a + this.b;
  }
}

3.初始化列表
在构造函数体执行之前除了可以调用超类构造函数之外,还可以初始化实例参数,初始化列表非常适合用来设置 final 变量的值。使用逗号分隔初始化表达式:

main() {
  var ins = new TestClass({
    'a': 6,
    'b': 7
  });
  print(ins.add());
  //输出:
  // parent construtor
  // child constructor
  // 13
}
class ParentClass {
  ParentClass.doPrint() {
    print('parent construtor');
  }
}
class TestClass extends ParentClass {
  final num a;
  final num b;
  TestClass(Map numMap)
    : a = numMap['a'],
      b = numMap['b'],
      super.doPrint() {
    print('child constructor');
  }
  add() {
    return this.a + this.b;
  }
}

构造函数执行顺序为:

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

4.重定向构造函数
有时候一个构造函数会调动类中的其他构造函数,用冒号连接调用的构造函数:

main() {
  new TestClass.doMath(); // sum: 13
}
class TestClass {
  num a;
  num b;  
  TestClass(this.a, this.b) {
    print('sum: ${add()}');
  }
  TestClass.doMath() : this(6, 7);
  int add() {
    return this.a + this.b;
  }
}

5.工厂方法构造函数
工厂构造函数是一种使用factory声明的构造函数,与普通构造函数不同,它不会自动生成一个实例对象,而是通过代码来决定要返回什么实例对象。比如,你希望每次新生成一个实例对象时就将其缓存起来,下次直接取缓存中的实例对象:

main() {
  var ins1 = new Symbol('s');
  var ins2 = new Symbol('s');
  print(identical(ins1, ins1)); // true
}
class Symbol {  
  final String name;  
  static Map<String, Symbol> _cache;  
   
  factory Symbol(String name) {  
    if (_cache == null) {  
      _cache = {};  
    }  
    // 从缓存中取实例对象,若缓存中没有,则新生成一个实例放入缓存中
    if (_cache.containsKey(name)) {  
      return _cache[name];  
    } else {  
      final symbol = new Symbol._internal(name);  
      _cache[name] = symbol;  
      return symbol;  
    }  
  }  
   
  Symbol._internal(this.name);  
} 

6. getter and setter
getter和setter是用来设置和访问对象属性的特殊函数,每个实例变量都隐含的具有一个 getter, 如果变量不是 final 的则还有一个 setter。

main() {
  var ins = new TestClass(4, 5, 6);
  print(ins.area); // 20
  ins.area = 10;
  print(ins.width); // 2.0

  print(ins.volume); // 60.0
  ins.volume = 240;
  print(ins.height); // 24.0
}

class TestClass {
  num width;
  num length;
  num height;
  
  TestClass(this.width, this.length, this.height);

  num get area => width * length;
  void set area(num value) => width = value / length;

  num get volume => area * height;
  void set volume(num value) => height = value / area;
}

7.隐式接口
Dart没有interface,但是每个类都隐式地定义了一个包含所有实例成员的接口,并且这个类实现了这个接口。如果你想创建类 A 来支持类 B 的 api,但又不希望拥有类 B 的实现,则类 A 应该implements 类B。所以在Dart中:

  • class即是interface;
  • 当class被当做interface用时,class中的方法就是接口的方法,需要在子类里重新实现,否则抛出异常;
  • 当class被当做interface用时,class中的成员变量也需要在子类里重新实现,否则抛出异常;

来看个例子:

main() {
  Person p = new Person();
  p.walk(); // a person has 2 legs for walking
}

class Animal {
  final int eyes;
  final int legs;

  Animal(this.eyes, this.legs);

  walk() {} 
  eat() {} 
}

class Person implements Animal {
  final int eyes = 2;
  final int legs = 2;
  eat() { 
    print('a person needs to eat for living'); 
  }
  walk() {
    print('a person has $legs legs for walking'); 
  }
}

8. 继承
前面的例子中已经有了一些继承的例子,继承很简单,和java没什么区别,所以不再详细介绍了。
我们来看下,extends和implements的区别:

  • 继承不需要在子类中全部重写超类中的实例变量和实例方法
  • 继承中子类会拥有超类的实现
  • Dart中是单继承,但是可以多实现

如下是继承的模式:

main() {
  Person p = new Person(2, 2);
  p.eat(); // animals need to eat for living
}

class Animal {
  final int eyes;
  final int legs;

  Animal(this.eyes, this.legs);

  walk() {} 
  eat() {
    print('animals need to eat for living');
  } 
}

class Person extends Animal {
  final int eyes;
  final int legs;

  Person(this.eyes, this.legs) : super(eyes, legs);
  
  walk() {
    print('a person has $legs legs for walking'); 
  }
}

9.Mixins
mixins是给类添加新的特性的方式,也是一种重用类代码的一种方式。mixins的关键字是with。但是有了继承,为什么还要Mixins呢? 主要是因为继承是单一的,而比如有些情况,两个子类继承不同的父类,但是两个字类又需要一些共同的方法,这时候mixins就发挥作用了。
比如下列例子中,人和麻雀分别继承不同的类,但是这两个类都需要一个唱歌的功能,现在就可以把唱歌通过混入的方式加入这两个类中:

main() {
  Sparrow sparrow = new Sparrow();
  sparrow.singASong(); // sing a song

  Person person = new Person();
  person.singASong(); // sing a song
}

abstract class Animal {
  void walk();
  void eat();
}

class Sing {
  void singASong() {
    print('sing a song');
  }
}
// 哺乳类继承动物类
class Mammal extends Animal {
  void walk() {
    print('mammals can walk'); 
  }
  void eat() {
    print('mammals need to eat');
  }
}
// 鸟类继承动物类
class Bird extends Animal {
  void walk() {
    print('birds can walk');
  }
  void eat() {
    print('birds need to eat for living');
  }
}
// 人继承自哺乳类,同时拥有唱歌的功能
class Person extends Mammal with Sing {}
// 麻雀继承自鸟类,也拥有唱歌的功能
class Sparrow extends Bird with Sing {}

ok,现在我们知道了Dart中的class有三种关系:extends、implements、with;
这三种关系可以同时使用,但是有先后顺序:extends -> mixins -> implements,extends在前,mixins在中间,implements最后。

抽象[8]

Java程序猿肯定很熟悉抽象了,但JS中没有抽象这一概念。抽象可以修饰类和方法。可以这样解释抽象类:有一个类A,它知道需要实现一些功能,但它不知道具体实现逻辑,也不知道要写多少个功能合适,所以就交给子类来具体实现。抽象类通常具有抽象函数。
抽象的意义:抽象类的存在意义就是为了被子类继承,抽象方法存在的意义就是为了被子类重写。
抽象类有以下几个特征:

  • 一个类如果继承了抽象类,这个类必须重写抽象类的所有抽象方法,只要有一个抽象方法没有被重写,子类也必须定义为抽象类。
  • 一个类如果拥有了抽象方法,这个类必须定义成抽象类。
  • 一个类要变成抽象类,在类名前面加abstract关键字即可。要定义一个抽象方法,不要用 { } 即可。抽象类不能被实例化

举个栗子:

// 定义一个抽象类,里面有两个抽象方法
// 如果类里面有抽象方法,但是类没有被abstract修饰,则抛出异常
abstract class Animal {
  void walk();
  void eat();
}
// 子类继承抽象类,需要全部重写抽象方法
class Person extends Animal {
  void eat() {
    print('a person needs to eat for living');
  }
  void walk() {
    print('a person has 2 legs for walking'); 
  }
}
// 子类继承抽象类,如果有任何一个抽象方法没重写,则子类也必须定义为抽象类
abstract class Bird extends Animal {
  void eat() {
    print('birds need to eat for living');
  }
}

枚举[9]

枚举其实是一种特殊的类,用来表现一个固定数目的常量。使用enum关键字来定义枚举类型:

enum Color {
  red,
  green,
  blue
}

枚举类型中的每个值都有一个 index getter 函数, 该函数返回该值在枚举类型定义中的位置(从 0 开始)。 例如,第一个枚举值的位置为 0, 第二个为 1:

main () {
  print('red position: ${Color.red.index}'); // 0
  print('green position: ${Color.green.index}'); // 1
  print('blue position: ${Color.blue.index}'); // 2
}

枚举的 values 常量可以返回所有的枚举值。

List<Color> colors = Color.values;
print(colors); // [Color.red, Color.green, Color.blue]

枚举类型具有如下的限制:

  • 无法继承枚举类型、无法使用 mixin、无法implements一个枚举类型
  • 无法显示的初始化一个枚举类型

泛型[10]

泛型是任何强类型语言都拥有的一种机制,用<>来标注类型。
使用泛型有两个优点:

  1. 必须指定数据的类型,类型安全,更易解读;
  2. 避免代码重复,使代码更简洁;

如:定义一个成员类型为String的List:

var names = new List<String>();
names.add('Tom'); // ok
names.add(34); 
// 报错:The argument type 'int' can't be assigned to the parameter type 'String'.

使用集合字面量:

var pages = <String, String>{
  'index.html': 'Homepage',
  'robots.txt': 'Hints for web robots',
  'humans.txt': 'We are people, not machines'
};

泛型可以让单一类适配多种类型的数据操作,比如:

main() {
  var k = new IntCache();
  k.setByKey('Tom', 22); // 如果是k.setByKey('Tom', '22');则会报错 
  print(k.getByKey('Tom'));

  var j = new StringCache();
  j.setByKey('Jack', 'math');
  print(j.getByKey('Jack'));
}

class IntCache {
  Map<String, int> map = new Map();
  int getByKey(String key) {
    return map[key];
  }
  void setByKey(String key, int value) {
    map.addAll({key: value});
  }
}
// 上面代码是对int类型操作,在没用泛型的情况下,你想对String类型操作,就得重新定义一个类
class StringCache {
  Map<String, String> map = new Map();
  String getByKey(String key) {
    return map[key];
  }
  void setByKey(String key, String value) {
    map.addAll({key: value});
  }
}

泛型通过将类型参数化,实现一个类可以适配多种数据类型操作,这样写就可以完美解决上述的问题:

class Cache<T> {
  Map<String, T> map = new Map();
  T getByKey(String key) {
    return map[key];
  }
  void setByKey(String key, T value) {
    map.addAll({key: value});
  }
}

在构造函数中使用泛型:

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

在运行时也可以判断具体的类型:

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

限制泛型类型:当使用泛型类型的时候,你可能想限制泛型的具体类型。 使用 extends 可以实现这个功能:

main() {
  var z = new Cache();
  z.setByKey('Jack', 'math'); // 报错
  print(z.getByKey('Jack'));
}

class Cache<T extends int> {
  Map<String, T> map = new Map();
  T getByKey(String key) {
    return map[key];
  }
  void setByKey(String key, T value) {
    map.addAll({key: value});
  }
}

异步机制[11]

Dart 有一些语言特性来支持异步编程。最常见的特性是async方法和await表达式。
Dart 库中有很多返回Future或者Stream对象的方法。这些方法是异步的: 这些函数在设置完基本的操作后就返回了,而无需等待操作执行完成。 例如读取一个文件,在打开文件后就返回了。
1. Future
Future和ES6中的Promise别无二致,来看个关于Future的例子:

main() {
  testFuture();
  // 4秒后输出:
  // hello world!
  // future done
}
testFuture() {
  // 延迟4秒
  Future.delayed(new Duration(seconds: 4), () {
    return 'hello world!';
  }).then((data) {
    print(data);
  }).catchError((e) {
    print(e);
  }).whenComplete(() {
    //无论成功或失败都会走到这里
    print('future done');
  });
}

Future提供了wait方法来实现多个异步操作的等待,这看起来很像是ES6中的Promise.all,还提供了any方法实现第一个异步操作完成后的处理,类似Promise.race。具体可看API:https://api.dartlang.org/stable/2.3.0/dart-async/Future-class.html

main() {
  testFuture();
  // 3秒后输出:
  // future wait
}
testFuture() {
  Future.wait([
    Future.delayed(new Duration(seconds: 3), () {
      return 'future';
    }),
    Future.delayed(new Duration(seconds: 1), () {
      return 'wait';
    })
  ]).then((results) {
    print('${results[0]} ${results[1]}');
  }).catchError((e) {
    print(e);
  });
}

再来看看asyncawait的用法,和es6一样,只不过async修饰在方法名后,es6中修饰在方法名前:

Future<String> delayPrint() {
  return Future.delayed(new Duration(seconds: 4), () {
    return 'test async await';
  });
}
testAsyncAwait() async {
  String str = await delayPrint();
  print(str);
}

2. Stream
Stream也是用于接收异步事件数据,和Future不同的是,它可以接收多个异步操作的结果(成功或失败)。也就是说,在执行异步任务时,可以通过多次触发成功或失败事件来传递结果数据或错误异常。Stream 常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。举个例子:

main() {
  testStream();
  // 依次输出以下内容(在各自延迟时间之后输出):
  // second Error
  // first Error
  // hello 1
  // hello 3
  // Stream done
}
testStream() {
  Stream.fromFutures([
    // 1秒后返回结果
    Future.delayed(new Duration(seconds: 2), () {
      return 'hello 1';
    }),
    // 抛出一个异常
    Future.delayed(new Duration(seconds: 1), () {
      throw AssertionError('first Error');
    }),
    // 3秒后返回结果
    Future.delayed(new Duration(seconds: 3), () {
      return 'hello 3';
    }),
    Future.delayed(new Duration(milliseconds: 300), () {
      throw AssertionError('second Error');
    })
  ]).listen((data) {
    print(data);
  }, onError: (e) {
    print(e.message);
  }, onDone: () {
    print('Stream done');
  });
}

元数据(注解)[12]

这玩意儿在Java中叫“注解”,在JS中叫“装饰器”(目前还在ES提案中)。使用注解可以给你的代码添加一些额外信息和功能,Dart提供了三个可直接使用的注解: @deprecated@override
先简单介绍一下这俩:

  • @deprecated:如果一个方法或类上面被这个注解修饰,则代表这个方法或类不再建议使用。如图中所示,调用一个被@deprecated修饰的实例方法,编辑器会提示你不要使用该方法:


    image.png
  • @override:使用这个注解修饰方法,代表这个方法是对父类方法的重写。如果父类中没有这个方法,编辑器会给你提示:


    image.png

你也可以定义自己的注解:

class Todo {
  final String who;
  final String what;
  const Todo(this.who, this.what);
}

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

使用反射可以获取元数据信息:

main() {
  ClassMirror classMirror = reflectClass(TestMetaData);
  // 获取 class 上的元数据
  classMirror.metadata.forEach((metadata) {
    print(metadata.reflectee.who + ' ==> ' + metadata.reflectee.what);
  });
}

执行后输出:seth ==> make this do something

小结

要学习Dart,你也可以参考Dart的官方文档:https://dart.dev/guides/language/language-tour,本文主要参考的也是官方文档,但对官网的一些说明进行了更详细的解释。Dart中很多的特性可以深入研究,但是如果只是想快速上手开发,浅尝辄止便够了。如果本文中有任何问题,欢迎指出。


  1. http://dart.goodev.org/guides/language/language-tour#a-basic-dart-program%E4%B8%80%E4%B8%AA%E6%9C%80%E5%9F%BA%E6%9C%AC%E7%9A%84-dart-%E7%A8%8B%E5%BA%8F

  2. http://dart.goodev.org/guides/language/language-tour#keywords%E5%85%B3%E9%94%AE%E5%AD%97

  3. http://dart.goodev.org/guides/language/language-tour#variables%E5%8F%98%E9%87%8F

  4. http://dart.goodev.org/guides/language/language-tour#built-in-types%E5%86%85%E7%BD%AE%E7%9A%84%E7%B1%BB%E5%9E%8B

  5. http://dart.goodev.org/guides/language/language-tour#operators%E6%93%8D%E4%BD%9C%E7%AC%A6

  6. http://dart.goodev.org/guides/language/language-tour#functions%E6%96%B9%E6%B3%95

  7. http://dart.goodev.org/guides/language/language-tour#classes

  8. http://dart.goodev.org/guides/language/language-tour#abstract-classes

  9. https://www.infoq.cn/article/2014/12/dart-1.8-release

  10. http://dart.goodev.org/guides/language/language-tour#generics%E6%B3%9B%E5%9E%8B

  11. http://dart.goodev.org/guides/language/language-tour#asynchrony-support%E5%BC%82%E6%AD%A5%E6%94%AF%E6%8C%81

  12. http://dart.goodev.org/guides/language/language-tour#metadata%E5%85%83%E6%95%B0%E6%8D%AE