Flutter 开发之 Dart 语言

Dart 作为 Flutter 的开发语言,可以说,Dart 是 Flutter 的一部分,Google 的目标是想让 Flutter 兼容全平台的开发,一统天下,这也是 Google 的野心,不管最终是否成行,那 Flutter 的流行必然带动 Dart 。

一、Dart 语言概述

1.1 Dart 是什么

Dart 语言是由 Google 开发的网络编程语言,于 2011 年 10 月 10 日发布第一个里程碑版本 M1。其诞生的目的是为了让广大 C 类 OOP 程序员们克服 JavaScript 那”复杂”的语言特性,Dart 刚推出的时候,定位是替代 JS 做前端开发,后来逐步扩展到移动端和服务端。

1.2 Dart 特点

  • 在 Dart 中,一切都是对象,一切对象都是class的实例,哪怕是数字类型、方法甚至 null 都是对象,所有的对象都是继承自 Object
  • 虽然Dart是强类型语言,但变量类型是可选的因为Dart可以自动推断变量类型
  • Dart 支持范型,List <int >表示一个整型的数据列表,List <dynamic >则是一个对象的列表,其中可以装任意对象
  • Dart 支持顶层方法(如 main 方法),也支持类方法或对象方法,同时也可以在方法内部创建方法
  • Dart 支持顶层变量,也支持类变量或对象变量
  • Dart 没有 public protected private 等关键字,如果某个变量以下划线(_)开头,代表这个变量在库中是私有的
  • Dart 中变量可以以字母或下划线开头,后面跟着任意组合的字符或数字

1.3 Dart 能做什么

Dart 是 Flutter 的开发语言,Flutter 必须遵循 Dart 的语言特性。

二、关键字 变量 及数据类型

2.1 关键字

关键字表中的所有其他词都是保留词。不能使用保留词作为标识符。

  • 带有上 标1 的单词是 内置标识符。避免使用内置标识符作为标识符。如果尝试为类或类型名使用内置标识符,则会发生编译时错误。
  • 使用上 标2 的单词是在 Dart 的 1.0 版本之后添加的与异步支持相关的更新的、有限的保留字。在以 async, async*, 或者 sync* 标记的任何函数体中,不能使用 async, await, 或者 yield 作为标识符。

2.2 变量

变量 var,常量 final 或 const来

以下代码是 Dart 中定义变量的方法:

main() {
  var a = 1;
  int b = 10;
  String name = "hello dart";
  dynamic c = 0.8;
}

可以明确指定某个变量的类型,如 int bool String,也可以用 var 或 dynamic 来声明一个变量,Dart 会自动推断其数据类型。

变量的默认值
注意:没有赋初值的变量都会有默认值null

final & const

如果不想改变一个变量,使用 final 或 const,不要使用 var 或其他类型,一个被 final 修饰的变量只能被赋值一次,一个被 const 修饰的变量是一个编译时常量(const 常量毫无疑问也是 final 常量)。可以这么理解:final 修饰的变量是不可改变的,而 const 修饰的表示一个常量。
注意:实例变量可以是 fina l的但不能是 const 。

下面用代码说明:

var count = 10; 
final Num = count;  // final 只能赋值一次
const Num1 = 10; // const赋值必须是编译时常量
final 和 const 的区别:
  • 区别一:final 要求变量只能初始化一次,并不要求赋的值一定是编译时常量,可以是常量也可以不是。而 const 要求在声明时初始化,并且赋值必需为编译时常量。
  • 区别二:final 是惰性初始化,即在运行时第一次使用前才初始化。而 const 是在编译时就确定值了。

2.3 内建数据类型

Dart有如下几种内建的数据类型:

  • numbers
  • strings
  • booleans
  • lists(或者是arrays)
  • maps
  • runes(UTF-32字符集的字符)
  • symbols

下面用一段代码来演示以上各类数据类型:

main() {
  // numbers
  var a = 0;
  int b = 1;
  double c = 0.1;

  // strings
  var s1 = 'hello ';
  String s2 = "dart";
  String s3 = s1 + s2;

  // booleans
  var real = true;
  bool isReal = false;

  // lists
  var arr = [1, 2, 3, 4, 5];
  List<String> arr2 = ['hello', 'world', "123", "456"];
  List<dynamic> arr3 = [1, true, 'haha', 1.0];

  // maps
  var map = new Map();
  map['name'] = 'zhangsan';
  map['age'] = 10;
  Map m = new Map();
  m['a'] = 'a';

  //runes,Dart 中 使用runes 来获取UTF-32字符集的字符。String的 codeUnitAt and codeUnit属性可以获取UTF-16字符集的字符
  var clapping = '\u{1f44f}';
  print(clapping); // 打印的是拍手emoji的表情

  // symbols
  print(#s == new Symbol("s")); // true
}

三、函数

Dart 是一种真正的面向对象语言,所以即使函数也是对象,具有类型和功能。这意味着函数可以分配给变量或作为参数传递给其他函数。

3.1 函数的返回值

所有的函数都有返回值,如果没有指定 return 语句,那么该函数的返回值为 null。虽然 Dart 推荐给函数加上返回值,但是不加返回值的函数同样可以正常工作,另外你还可以用 => 代替 return 语句,比如下面的代码:

// 声明返回值
int add(int a, int b) {
  return a + b;
}

// 不声明返回值
add2(int a, int b) {
  return a + b;
}

// =>是return语句的简写
add3(a, b) => a + b; 

main() {
  print(add(1, 2)); // 3
  print(add2(2, 3)); // 5
  print(add3(1, 2)); // 3
}

3.2 命名参数

定义命名参数时,可以以 {type paramName} 或者 {paramName: type} 两种方式声明参数,而调用命名参数时,需要以 funcName(paramName: paramValue) 的形式调用。

sayHello({String name}) {
  print("hello, my name is $name");
}

sayHello2({name: String}) {
  print("hello, my name is $name");
}

main() {
  // 打印 hello, my name is zhangsan
  sayHello(name: 'zhangsan');

  // 打印 hello, my name is wangwu
  sayHello2(name: 'wangwu');
}

命名参数的参数并不是必须的,所以上面的代码中,如果调用 sayHello() 不带任何参数,也是可以的,只不过最后打印出来的结果是:hello, my name is null.
在 Flutter 开发中,可以使用 @required 注解来标识一个命名参数,这代表该参数是必须的,不传则会报错,比如下面的代码:

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

3.3 位置参数

使用中括号 [] 括起来的参数是函数的位置参数,代表该参数可传可不传,位置参数只能放在函数的参数列表的最后面,如下代码所示:

sayHello(String name, int age, [String hobby]) { // 位置参数可以有多个,比如[String a, int b]
  StringBuffer sb = new StringBuffer();
  sb.write("hello, this is $name and I am $age years old");
  if (hobby != null) {
    sb.write(", my hobby is $hobby");
  }
  print(sb.toString());
}

main() {
  // hello, this is zhangsan and I am 20 years old
  sayHello("zhangsan", 20);
  // hello, this is zhangsan and I am 20 years old, my hobby is play football
  sayHello("zhangsan", 20, "play football");
}

3.4 参数默认值

可以为命名参数或者位置参数设置默认值,如下代码所示:

// 命名参数的默认值
int add({int a, int b = 3}) { // 不能写成:int add({a: int, b: int = 3})
  return a + b;
}

// 位置参数的默认值
int sum(int a, int b, [int c = 3]) {
  return a + b + c;
}

3.5 main() 函数

不论在 Dart 还是Flutter中,必须都需要一个顶层的 main() 函数,它是整个应用的入口函数,main() 函数的返回值是 void,还有一个可选的参数,参数类型是 List < String > 。

函数作为一类对象,可以将一个函数作为参数传给另一个函数,比如下面的代码:

printNum(int a) {
  print("$a");
}

main() {
  //  依次打印:
  //  1
  //  2
  //  3
  var arr = [1, 2, 3];
  arr.forEach(printNum);
}

你也可以将一个函数赋值给某个变量,比如下面的代码:

printNum(int a) {
  print("$a");
}

main() {
  var f1 = printNum;
  Function f2 = printNum;
  var f3 = (int a) => print("a = $a");
  f1(1);
  f2(2);
  f3(6);
}

3.6 匿名函数

大多数函数都是有名称的,比如 main() printName() 等,但是也可以写匿名函数,如果对 Java 比较熟悉,那下面的 Dart 代码肯定也不会陌生:

test(Function callback) {
  callback("hello");
}

main() {
  test((param) {
    // 打印hello
    print(param);
  });
}

匿名函数类似于 Java 中的接口,往往在某个函数的参数为函数时使用到。

四、运算符

Dart 中的运算符与 Java 中的类似,比如:

++a a == b 
c ? a : b

但是也有一些与 Java 不太一样的运算符,下面用代码说明:

main() {
  // 与Java相同的运算符操作

  int a = 1;
  ++a;
  a++;
  var b = 1;
  print(a == b);  // false
  print(a * b); // 3
  bool real = false;
  real ? print('real') : print('not real'); // not real
  print(real && a == b); // false
  print(real || a == 3); // true
  print(a != 2); // true
  print(a <= b); // false
  var c = 9;
  c += 10;
  print("c = $c"); // c = 19
  print(1<<2); // 4

  // 与Java不太一样的运算符操作

  // is运算符用于判断一个变量是不是某个类型的数据
  // is!则是判断变量不是某个类型的数据
  var s = "hello";
  print(s is String); // true
  var num = 6;
  print(num is! String); // true

  // ~/才是取整运算符,如果使用/则是除法运算,不取整
  int k = 1;
  int j = 2;
  print(k / j); // 0.5
  print(k ~/ j); // 0

  // as运算符类似于Java中的cast操作,将一个对象强制类型转换
  (emp as Person).teach();

  // ??=运算符 如果 ??= 运算符前面的变量为null,则赋值,否则不赋值
  var param1 = "hello", param2 = null;
  param1 ??= "world";
  param2 ??= "world";
  print("param1 = $param1"); // param1 = hello
  print("param2 = $param2"); // param2 = world

  // ?.运算符
  var str1 = "hello world";
  var str2 = null;
  print(str1?.length); // 11
  print(str2?.length); // null 
  print(str2.length); // 报错
}

..运算符(级联操作)

如果对 Java 中的建造者模式比较熟悉的话,Dart 中的 .. 运算符也很好理解,先看下面的代码:

class Person {
  eat() {
    print("I am eating...");
  }

  sleep() {
    print("I am sleeping...");
  }

  study() {
    print("I am studying...");
  }
}

main() {
  // 依次打印
  //  I am eating...
  //  I am sleeping...
  //  I am studying...
  new Person()..eat()
      ..sleep()
      ..study();
}

可以看到,使用 .. 调用某个对象的方法(或者成员变量)时,返回值是这个对象本身,所以你可以接着使用 .. 调用这个对象的其他方法,这不就类似于 Java 中的建造者模式,每次 build 某个属性时,都返回一个 this 对象。

五、流程控制语言

流程控制在大部分语言中大同小异,在 Dart 中只有最后一个 assert 比较特殊,其实 assert 在 C 语言中有使用。

可以使用以下任何一种方法来控制 Dart 代码的流:

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

也可以使用 try-catch 和 throw 来对控制流程作出改变。

if / else switch for /while try / catch 语句跟 Java 中都类似,try / catch 语句可能稍有不同,下面用一段代码说明:
main() {
  // if else语句
  int score = 80;
  if (score < 60) {
    print("so bad!");
  } else if (score >= 60 && score < 80) {
    print("just so so!");
  } else if (score >= 80) {
    print("good job!");
  }

  // switch语句
  String a = "hello";
  // case语句中的数据类型必须是跟switch中的类型一致
  switch (a) {
    case "hello":
      print("haha");
      break;
    case "world":
      print("heihei");
      break;
    default:
      print("WTF");
  }

  // for语句
  List<String> list = ["a", "b", "c"];
  for (int i = 0; i < list.length; i++) {
    print(list[i]);
  }
  for (var i in list) {
    print(i);
  }
  // 这里的箭头函数参数必须用圆括号扩起来
  list.forEach((item) => print(item));

  // while语句
  int start = 1;
  int sum = 0;
  while (start <= 100) {
    sum += start;
    start++;
  }
  print(sum);

  // try catch语句
  try {
    print(1 ~/ 0);
  } catch (e) {
    // IntegerDivisionByZeroException
    print(e);
  }

  try{
    var str = null;
    assert(str != null);
  } catch(e){
    print('捕获错误Error: $e');
  } finally {
    print('最终执行');
  }

  try {
    1 ~/ 0;
  } on IntegerDivisionByZeroException { // 捕获指定类型的异常
    print("error"); // 打印出error
  } finally {
    print("over"); // 打印出over
  }
}

六、面向对象

Dart 是一种面向对象的语言,具有类和基于 mixin 的继承。

6.1 类的定义与构造方法

Dart 中的类没有访问控制,所以不需要用 private, protected, public 等修饰成员变量或成员函数,一个简单的类如下代码所示:

class Person {
  String name;
  int age;
  String gender;
  Person(this.name, this.age, this.gender);
  sayHello() {
    print("hello, this is $name, I am $age years old, I am a $gender");
  }
}

上面的 Person 类中有 3 个成员变量,一个构造方法和一个成员方法,看起来比较奇怪的是 Person 的构造方法,里面传入的 3 个参数都是 this.xxx,而且没有大括号 {} 包裹的方法体,这种语法是 Dart 比较独特而简洁的构造方法声明方式,它等同于下面的代码:

Person(String name, int age, String gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
}

要调用 Person 类的成员变量或成员方法,可以用下面的代码:

  var p = new Person("zhangsan", 20, "male");
  p.sayHello(); // hello, this is zhangsan, I am 20 years old, I am a male
  p.age = 50;
  p.gender = "female";
  p.sayHello(); // hello, this is zhangsan, I am 50 years old, I am a female

类除了有跟类名相同的构造方法外,还可以添加命名的构造方法,如下代码所示:

class Point {
  num x, y;
  Point(this.x, this.y);
  // 类的命名构造方法
  Point.origin() {
    x = 0;
    y = 0;
  }
}

main() {
  // 调用Point类的命名构造方法origin()
  var p = new Point.origin();
  var p2 = new Point(1, 2);
}

Dart 中使用 extends 关键字做类的继承,如果一个类只有命名的构造方法,在继承时需要注意,如下代码:

class Human {
  String name;
  Human.fromJson(Map data) {
    print("Human's fromJson constructor");
  }
}

class Man extends Human {
  Man.fromJson(Map data) : super.fromJson(data) {
    print("Man's fromJson constructor");
  }
}

由于 Human 类没有默认构造方法,只有一个命名构造方法 fromJson,所以在 Man 类继承 Human 类时,需要调用父类的 fromJson 方法做初始化,而且必须使用 Man.fromJson(Map data) : super.fromJson(data) 这种写法,而不是像 Java 那样将 super 写到花括号中。
有时候仅仅只是在某个类的构造方法中,调用这个类的另一个构造方法,可以这么写:

class Point {
  num x, y;
  Point(this.x, this.y);
  // 命名构造方法调用了默认的构造方法
  Point.alongXAxis(num x) : this(x, 0);
}

6.2 类的成员方法

一个类的成员方法是一个函数,为这个类提供某些行为。上面的代码中已经有了一些类的成员方法的定义,这些定义方式跟 Java 很类似,可以为某个类的成员变量提供 getter/setter 方法,如下代码:

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

  // 构造方法传入left, top, width, height几个参数
  Rectangle(this.left, this.top, this.width, this.height);

  // right, bottom两个成员变量提供getter/setter方法
  num get right => left + width;
  set right(num value) => left = value - width;
  num get bottom => top + height;
  set bottom(num value) => top = value - height;
}

6.3 抽象类和抽象方法

使用 abstract 修饰一个类,则这个类是抽象类,抽象类中可以有抽象方法和非抽象方法,抽象方法没有方法体,需要子类去实现,如下代码:

abstract class Doer {
  // 抽象方法,没有方法体,需要子类去实现
  void doSomething();
  // 普通的方法
  void greet() {
    print("hello world!");
  }
}

class EffectiveDoer extends Doer {
  // 实现了父类的抽象方法
  void doSomething() {
    print("I'm doing something...");
  }
}

七、泛型

Java 和 C++ 语言都有泛型,Dart 语言也不例外,使用泛型有很多好处,比如:

  • 正确指定泛型类型会产生更好的生成代码
  • 泛型可以减小代码的复杂度

Dart内置的数据类型List < E > 就是一个泛型数据类型,你可以往 List 中塞任何你想的数据类型比如整型、字符串、布尔值等。根据约定,类型变量具有单字母名称,如E、T、S、K和V。

  • E 代表 Element 元素
  • T 代表 Type 类型
  • K 代表 Key 键
  • V 代表 Value 值
main(){

  var names = <String>['张三','李四','王五'];

  var pages = <String,String>{
    'index.html':'Homepage',
    'about.html':'About',
  };

  var veiws = Map<int,Object>();

  var lists = List<String>();
  lists.addAll(['张三','李四','王五']);
//  lists.add(20);//报错


}

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

//String缓存
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);
}

八、异步处理

Dart 语言是目前少数几个支持异步操作的语言,一般使用 async 函数和 await 表达式实现异步操作。Dart 提供了类似 ES7 中的 async await 等异步操作,这种异步操作在 Flutter 开发中会经常遇到,比如网络或其他 IO 操作,文件选择等都需要用到异步的知识。

async 和 await 往往是成对出现的,如果一个方法中有耗时的操作,你需要将这个方法设置成 async,并给其中的耗时操作加上 await 关键字,如果这个方法有返回值,你需要将返回值塞到 Future 中并返回,如下代码所示:

Future checkVersion() async {
  var version = await lookUpVersion();
  // Do something with version
}

下面的代码使用 Dart 从网络获取数据并打印出来:

import 'dart:async';
import 'package:http/http.dart' as http;

Future<String> getNetData() async{
  http.Response res = await http.get("http://www.baidu.com");
  return res.body;
}

main() {
  getNetData().then((str) {
    print(str);
  });
}

Dart库充满了返回 Future 或 Stream 对象的函数。这些功能是异步的:它们在设置可能耗时的操作(例如 I/O)之后返回,而不等待该操作完成。 在 async 和 await 关键字支持异步编程,让你写异步代码看起来类似于同步代码。
使用 async/await 之前需要引入 dart:async 模块。

import 'dart:async';

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

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

九、类型定义

在 Dart 中,函数是对象,就像字符串和数字是对象一样。typedef 或 function-type 为函数提供一个类型别名,可以在声明字段和返回类型时使用这个名称。当函数类型被分配给变量时,typedef 保留类型信息。

以下代码不使用 typedef:

class SortedCollection {
  Function compare;

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

// Initial, broken implementation.
int sort(Object a, Object b) => 0;

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

  // All we know is that compare is a function,
  // but what type of function?
  assert(coll.compare is Function);
}

当给 compare 分配f时类型信息会丢失。f 的类型是(Object, Object)->int(int表示返回值类型),当然 compare 的类型是 Function。如果我们更改代码以使用显式名称和保留类型信息,开发人员和工具都可以使用这些信息。

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

class SortedCollection {
  Compare compare;

  SortedCollection(this.compare);
}

// Initial, broken implementation.
int sort(Object a, Object b) => 0;

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

注意:目前,typedefs 仅限于函数类型。我们期望这种情况会改变。

因为 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!
}

十、异常处理

Dart 代码可以抛出和捕获异常。异常是指程序执行中发生的意料之外的错误。如果没有捕获异常,引发异常的隔离程序将被挂起,通常隔离程序及其程序将被终止。

与 Java 相反,Dart 的所有异常都是未检查的异常。方法不声明它们可能抛出哪些异常,也不要求您捕获任何异常。

Dart 提供 Exception 和 Error 类型,以及许多预定义的子类型。当然可以自己定义异常。但是,Dart 程序可以抛出任何非空对象不仅仅是异常和错误对象。

Throw
throw FormatException('Expected at least 1 section');  //你也可以抛出任意对象:

throw 'Out of llamas!';   //注意:在正式使用中通常抛出的是实现了Error或Exception类型的对象;

因为抛出异常是一个 表达式 ,您可以在=>语句中抛出异常,也可以在任何允许表达式的地方抛出异常:

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

捕获异常将阻止异常传播(除非重新抛出异常)。捕获异常后我们可以去判断并处理相应的异常。

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

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

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // A specific exception
  buyMoreLlamas();
} on Exception catch (e) {
  // Anything else that is an exception
  print('Unknown exception: $e');
} catch (e) {
  // No specified type, handles all
  print('Something really unknown: $e');
}

正如前面的代码所示,可以使用 on 或 catch 或 both。在需要指定异常类型时使用。当您的异常处理程序需要异常对象时,请使用 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 error
  } catch (e) {
    print('misbehave() partially handled ${e.runtimeType}.');
    rethrow; // Allow callers to see the exception.
  }
}

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

要确保在抛出异常时运行某些业务代码,请使用 finally 子句。如果没有 catch 子句匹配异常,则在 finally 子句运行后传播异常:

try {
  breedMoreLlamas();
} finally {
  // Always clean up, even if an exception is thrown.
  cleanLlamaStalls();
}

finally 子句在所有匹配到的 catch 子句之后运行:

try {
  breedMoreLlamas();
} catch (e) {
  print('Error: $e'); // Handle the exception first.
} finally {
  cleanLlamaStalls(); // Then clean up.
}

Refer