三、Dart语法

Dart 是支持基于 mixin 继承机制的语言(JavaScript 是基于原型的),所有对象都是一个类的实例,而除了 null 以外所有的类都继承自 Object 类。

定义类的属性和方法

// 类的定义不能写在 main 函数里
class Person {
  String? name; // Declare instance variable name, initially null.
  int age = 0; // Declare y, initially 0.
  void getInfo() {
    print('${this.name} ------ ${this.age}');
  }
  void setName(String name) {
    this.name=name;
  }
}
void main(List<String> args) {
  var p = new Person();// 可以省略 new
  p.getInfo();
  p.setName('my name');
  p.getInfo();
}

实例属性如果没有初始化的话,默认是 null。

所有实例变量都会隐式声明 Getter 方法。可以修改的实例变量和 late final声明但是没有初始化的变量还会隐式声明一个 Setter 方法,我们可以通过 getter 和 setter 读取或设置实例对象。

class Person {
  String? name;
  int age = 0;
  late final int height;
}

void main(List<String> args) {
  var p = new Person();
  p.name = 'my name';// setter
  p.height = 180;// setter
  print(p.name);// getter
  print(p.height);// getter
  p.height = 190; // Field 'height' has already been initialized.
}

实例变量可以是 final 的,在这种情况下只能被set 一次。

构造函数

用一个与类名一样的函数就可以创建构造函数,还有一种命名式构造函数。

class Point {
  double x = 0;
  double y = 0;
  // Point(double x, double y) {
  //   this.x = x;
  //   this.y = y;
  // }
  // 上面构造函数的语法糖可以写成这样
  Point(this.x, this.y);
  // 命名式构造函数——使用初始化列表
  Point.origin(double xOrigin, double yOrigin):x=xOrigin,y=yOrigin
  // 命名式构造函数还可以这么写
  // Point.origin(double this.x, double this.y);
}
// 在 main 中使用
void main(List<String> args) {
  // 使用命名构造函数
  var p1 = new Point.origin(10, 20);
  var p2 = Point(10, 20);
}

使用 this 关键字引用当前实例。

如果没有声明构造函数,Dart 会自动生成一个没有参数的构造函数并且这个构造函数会调用其父类的无参数构造方法。

构造函数不会被继承,也就是说子类没办法继承父类的构造函数。命名式构造函数也不能被继承。

命名构造函数可以有多个,当实例化时根据需要直接调用就行了。

初始化列表

在构造函数运行之前,有一个初始化列表的概念。可以初始化实例变量

class Rect {
  int height;
  int width;
  Rect()
      : width = 10,
        height = 10 {
    print("${this.width}---${this.height}");
  }
  Rect.create(int width, int height)
      : width = width,
        height = height {
    print("${this.width}---${this.height}");
  }
}

void main(List<String> args) {
  var p1 = Rect(); // 10---10

  var p2 = Rect.create(100, 200); // 100---200
}

当使用 Rect 构造时,初始化列表会给 width 和 height 初始化为 10。

当使用Rect.create构造时,初始化列表会通过用传入的值来初始化。

初始化列表可以解决一个类变量既是final的又是可选参数赋值的且可选参数默认值需要动态确定的问题。

class Person {
  static final String name = 'myname';
  final int age;
  Person() : age = Person.name == 'myname' ? 10 : 20;
}

上面的代码 age 是可选的且age 的默认值需要动态确定,这时候就没办法使用可选参数了,如以下示例:

class Person {
  static final String name = 'myname';
  final int age;
  // ❌ The default value of an optional parameter must be constant.
  Person([this.age = Person.name == 'myname' ? 10 : 20]);
}

重定向构造函数

当调用一个构造函数时,让这个构造函数能够调用另外一个构造函数,这就叫重定向构造函数。

下面的代码中,当调用Person.init()时,会重定向到Person这个构造函数中,本质上是初始化列表。

class Person {
  String name;
  int age;
  Person(this.name, this.age);
  Person.init(String name, int age) : this(name, age);
}

常量构造函数

当类的变量是 final的,这时候就需要用const修饰构造函数,否则实例化时会报错。

下面例子中会创建两个相等的对象,类似于单例模式。

class Person {
  final String name;
  const Person(this.name);
}

void main(List<String> args) {
  const p1 = const Person('myname');
  const p2 = Person('myname'); // const 可以省略
  print(identical(p1, p2));// 这两个对象是相等的
}

工厂构造函数

普通构造函数会自动返回对象,工厂构造函数最大的特点是可以手动返回一个对象。

在工厂构造函数中无法访问 this

下面使用工厂构造函数能够主动返回一个单例

class Person {
  String name;

  static final Map<String, Person> cache = {};

  Person(this.name);

  factory Person.getSingle(String name) {
    if (cache.containsKey(name)) {
      return cache[name] as Person;
    } else {
      cache[name] = new Person(name);
      return cache[name] as Person;
    }
  }
}

void main(List<String> args) {
  var p1 = Person.getSingle('qyx');
  var p2 = Person.getSingle('qyx');
  print(identical(p1, p2));// true
}

实例的私有属性/方法

将类抽离成一个文件,并在属性或者方法前加_就能定义实例对象的私有变量。

lib/Person.dart

class Person {
  String? name; // Declare instance variable name, initially null.
  int _age = 0; // Declare y, initially 0.
  void getInfo() {
    print('${this.name} ------ ${this._age}');
  }
}

main.dart

import 'lib/Person.dart';

void main(List<String> args) {
  var p = Person();
  // print(p._age); 无效
  p.getInfo();
}

Getter 和 Setter

构造函数自动会设置实例变量的 getter 和 setter ,我们也可以手动指定,这种方式的好处是可以监听属性。

class Rect {
  int height;
  int width;
  Rect(this.width, this.height);
  // 手动指定 getter 的写法
  get area {
    return this.height * this.width;
  }
  // 手动指定 setter 的写法
  set h(int value) {
    print("当你调用 xx.h 时,会打印这段话,表示你已经被监听到了");
    this.height = value;
  }

  set w(int value) {
    this.width = value;
  }
}

void main(List<String> args) {
  var p = Rect(10, 20);
  print(p.area);// getter
  p.h = 100;// setter
  p.w = 100;
  print(p.area);
}

静态成员

跟 TS 一样,使用 static 来声明静态成员。

class Rect {
  static int height = 10;
  static int width = 10;
  static getArea() {
    print(height * width);
  }
}

void main(List<String> args) {
  Rect.getArea();
}

有两点需要注意:

  • 静态成员不能访问实例变量

      class Rect {
      int height = 10;
      static int width = 10;
      static getArea() {
          print(this.height * width); // 报错了 不能访问 实例属性 height
      }
      }
    
  • 实例方法可以访问静态成员

      class Rect {
      int height;
      static int width = 10;
      Rect(this.height);
      getArea() {
          print(this.height * width);// 如果访问实例属性,推荐加上 this。
      }
      }
    
      void main(List<String> args) {
      new Rect(10).getArea();
      }
    

继承

构造函数不能被继承,使用 extendssuper 关键字来继承父类的属性和方法。

纯继承父类

class Animal {
  String name;
  void sound(voice) {
    print(voice);
  }

  Animal(this.name);
}

class Dog extends Animal {
  Dog([String name = 'dog']) : super(name);
}

void main(List<String> args) {
  var dog = new Dog();
  print(dog.name); // dog
  dog.sound('汪汪'); // 汪汪
}

其中Dog([String name = 'dog']) : super(name);需要解释一下:

  • : super(name)这种语法是用初始化列表在构造 Dog 时调用其父类的构造函数来设置 name
  • Dog([String name = 'dog'])这种语法是调用new Dog()name 是可选的,默认值为dog

扩展子类的属性和方法

class Animal {
  String name;
  void sound(voice) {
    print(voice);
  }

  Animal.create(this.name);
}

class Dog extends Animal {
  String sex;
  Dog(this.sex, [String name = 'dog']) : super.create(name);
  void run() {
    print('${this.name} runrun');
  }
}

重写父类的属性和方法

class Animal {
  String name;
  void sound(voice) {
    print(voice);
  }

  Animal.create(this.name);
}

class Dog extends Animal {
  String sex;
  Dog(this.sex, [String name = 'dog']) : super.create(name);
  void run() {
    print('${this.name} runrun');
  }

  @override
  void sound(voice) {
    print('${this.name} $voice');
  }
}

void main(List<String> args) {
  var dog = new Dog('雄');
  print(dog.name); // dog
  dog.sound('汪汪'); //dog 汪汪
}

推荐使用@override来重写父类的属性和方法

子类中调用父类的方法

通过 super 来调用父类的方法

class Dog extends Animal {
  String sex;
  Dog(this.sex, [String name = 'dog']) : super.create(name);
  void run() {
    super.sound('汪汪');
    print('${this.name} runrun');
  }
}

抽象类

  • 抽象类主要用于定义标准

  • 抽象类不能被实例化,只有继承它的子类才可以被实例化

  • 抽象类如果想被实例化,可以用工厂构造函数

使用abstract关键字表示这是抽象类。

比如下面定义一个 Animal 的抽象类,这里面有所有动物的标准。

abstract class Animal {
  sound(); // 抽象方法
  print() {} // 普通方法 可以不被子类实现
}

// 子类中必须实现同样的抽象方法
class Dog extends Animal {
  @override
  sound() {}
}

多态

多态就是同一操作作用于不同的对象时,可以产生不同的解释和不同的效果。

JavaScript 中是用原型链的方式来实现多态的,比如 ObjectArray 的原型上都有 toString 方法,本质上是在Array.prototype写了一个toString来覆盖Object.prototype的原型上的toString

Dart中的多态是通过子类重写父类定义的方法,这样每个子类都有不同的表现。

使用抽象类的话就只需要定义父类的方法而不用实现,让继承它的子类去实现,每个子类就是多态的。

abstract class Animal {
  sound(); // 抽象方法
}

class Dog extends Animal {
  @override
  sound() {
    print('汪汪');
  }

  run() {}
}

class Cat extends Animal {
  @override
  sound() {
    print('喵喵');
  }

  run() {}
}

void main(List<String> args) {
  var dog = new Dog();
  var cat = new Cat();
  print(dog.sound());
  print(cat.run());
  // 下面两个不能调 run 方法
  Animal _dog = new Dog();
  Animal _cat = new Cat();
}

接口

dart 中没有 interface,我们使用抽象类来定义接口,使用implements来让类匹配接口。

类匹配单个接口

比如下面使用抽象类来封装统一的 增删改查 功能

abstract class Db {
  String uri;
  add();
  remove();
  save();
  select();
}

使用implements匹配接口

class MySql implements Db {
  @override
  add() {}

  @override
  remove() {}

  @override
  save() {}

  @override
  select() {}
}

上面的代码也可以用 extends 关键字来继承后重写。一般情况下我们这么用:

  • 如果需要有共同的方法复用,我们用 extends

  • 如果需要一个规范约束,那就使用 implements

类匹配多个接口

abstract class A {
  late String name;
  getA();
}

abstract class B {
  getB();
}

class C implements A, B {
  @override
  getA() {}

  @override
  getB() {}

  @override
  late String name;
}

mixins混入

使用 mixins 可以实现类似多继承的功能,mixins 用关键字 withmixin

mixin A {
  void getA() {}
}

mixin B {
  void getB() {}
}

class C with A, B {}

void main(List<String> args) {
  var c = new C();
  c.getA();
  c.getB();
}

上面的代码混入(mixins)了多个mixins类的实例方法。

  • mixins 的类只能继承自 Object,不能继承其他类。
class A {
  void getA() {}
}

class B extends A { 
  void getB() {}
}

class C with A, B {} // ❌报错,B 是被 mixins 的类,不能继承

为了让 mixins 类更加直观,推荐使用 mixin 关键字来定义 mixin 类

mixin A {
  void getA() {}
}

mixin B extends A { // ❌报错,B 是被 mixins 的类,不能继承
  void getB() {}
}

class C with A, B {}
  • mixins 的类不能有构造函数
mixin A {
  void getA() {}
}

mixin B {
  B(); // ❌报错 B 是被 mixins 的类,不能有构造函数
  void getB() {}
}

class C with A, B {}
  • 一个类可以 mixins 多个 mixins 类

  • 一个类可以继承某个类再 mixins 一些 mixins 类

class A {
  void getA() {}
}

class B {
  void getB() {}
}

class C extends A with B {}
  • mixins 不是继承,也不是接口,当使用 mixins 后,相当于创建了一个超类,能够兼容下所有类
class A {
  void getA() {}
}

mixin B {
  void getB() {}
}

class C extends A with B {}

void main(List<String> args) {
  var c = new C(); 
  print(c is A);// true
  print(c is B);// true
  print(c is C);// true
}
  • 使用 on 关键字可以指定哪些类可以使用该 Mixin 类
class A {
  void getA() {}
}

mixin B on A {
  void getB() {}
}

// class C with B {}     ❌这样写是报错的
class C extends A with B {}

泛型

跟 TS 一样,Dart 也支持泛型,泛型就是泛用的类型,是一种将指定权交给用户的不特定类型。

比如下面的函数就由用户指定传入的类型。

  T getData<T>(T data) {
    return data;
  }

// 调用者可以指定类型
  getData<String>('123');
  getData<num>(123);
    getData<List>([1, 2, 3]);

泛型类

在实例化一个类时可以通过泛型来指定实例对象的类型。

下面就是实例化 List 后指定了List 对象属性值的类型。

  List l1 = new List<int>.filled(2, 1);
  List l2 = new List<String>.filled(2, '');
  • 定义泛型类
class A<T> {
  T age;
  A(T this.age);
  T getAge() {
    return this.age;
  }
}
  • 使用泛型类
void main(List<String> args) {
  // 使用泛型类
  var a = new A<int>(12);
  var b = A<String>('12');
}

泛型接口

泛型接口的定义方式就是接口跟泛型类的集合体,可以这么定义

// 泛型接口
abstract class Cache<T> {
  void setKey(String key, T value);
}
// 类匹配这个接口
class FileCache<T> implements Cache<T> {
  @override
  void setKey(String key, T value) {}
}

class MemoryCache<T> implements Cache<T> {
  @override
  void setKey(String key, T value) {}
}

使用时指定泛型的具体类型

  var f = new FileCache<String>();// 指定 String
  f.setKey('key', 'string');
  var m = new MemoryCache<int>();// 指定 int
  m.setKey('key', 123);

限制泛型

跟 Typescript 一样,泛型约束使用 extends 关键字。

abstract class Cache<T> {
  void setKey(String key, T value);
}
// 这里约束MemoryCache只能为 int
class MemoryCache<T extends int> implements Cache<T> {
  @override
  void setKey(String key, T value) {}
}
void main(List<String> args) {
  // var m = new MemoryCache<String>(); 这里就不能是 String 类型了
  var m = new MemoryCache<int>();
  m.setKey('key', 123);
}

enum

枚举的规则跟 Typescript 差别很大,最终目的还是用于定义常量值

  • 定义枚举
enum Colors { RED, GREEN, BLUE }
  • 访问枚举的下标
assert(Color.red.index == 0);
  • 获取全部枚举值
Colors.values // [Colors.RED, Colors.GREEN, Colors.BLUE]
  • 访问枚举值
Colors.RED // Colors.RED
  • 用下标访问枚举值
  assert(Colors.values[0] == Colors.RED);
  • 枚举值的类型
Colors.RED.runtimeType // Colors

Late 修饰符

Dart2.12 增加了 late 修饰符,它有两个用途:

  • 用来声明一个在声明后才初始化的且不能为 null 的变量
  • 懒初始化变量

Dart 的控制流分析可以检测不可为 null 的变量在使用之前何时设置为非 null 值,但有时分析会失败。两种常见情况是顶级变量和实例变量:Dart通常无法确定它们是否已设置,因此它不会尝试。

如果你确定变量被使用之前已经被设置了,但是 Dart 判断不一致,就可以使用 late 来消除报错

// The non-nullable variable 'a' must be initialized.
String a;
void main(List<String> args) {
  a = '123';
  print(a);
}

上面的代码中, a 是全局变量,Dart 没有办法分析全局变量是否被设置,因此上面的代码会报错。这时候可以用 late 语句来消除错误。

- String a;
+ late String a;
void main(List<String> args) {
  a = '123';
  print(a);
}

如果将变量标记为 late,但在其声明时对其进行初始化,则初始值设定项会在首次使用该变量时运行。这种惰性初始化在以下几种情况下非常方便:

  • 变量不一定会被使用,那么这种初始化非常节省内存
// This is the program's only call to _readThermometer().
late String temperature = _readThermometer(); // Lazily initialized.

上面的代码中,如果temperature变量一直没有被使用,那么_readThermometer函数不会被调用

  • 实例变量没有被初始化,但是又需要访问实例变量的时候添加 late
class Cache {
  late String name;
  void setName(String name) {
    this.name = name;
  }
}

上面的代码中,由于没有构造函数,那么实例时,name 属性并不会被初始化,这时候访问它会报错

  var m = new Cache();
  // ❌LateInitializationError: Field 'name' has not been initialized.
  print(m.name);

使用 import 来指定命名空间以便其它库可以访问。

import 的唯一参数是用于指定代码库的 URI。

对于 Dart 内置的库,使用 dart:xxxxxx 的形式。

对于其它的库,你可以使用一个文件系统路径或者以 package:xxxxxx 的形式。 package:xxxxxx 指定的库通过包管理器(比如 pub 工具)来提供。

import 'dart:math'; // 引入内置 math 库
import 'package:test/test.dart'; // 引入包管理器中的库
import 'lib/test.dart'; // 引入自己写的库

package 包

下载package使用需要在根目录手动创建pubspec.yaml文件,类似 npm 的 package.json,这是用来管理包的版本和依赖的。

创建后根据提示写入以下内容:

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

然后可以去这个网站查可以使用的包。

以 http 模块为例:

下载包

只有 dart 就用这个命令

$ dart pub add http

有 flutter 就用这个命令

$ flutter pub add http

上面的命令会在pubspec.yaml中增加版本依赖,并且隐式运行dart pub get or flutter pub get

dependencies: 
  http: ^0.13.4

使用包

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

库冲突

如果导入的两个代码库有同样的命名,则可以使用指定前缀as。比如如果 library1library2 都有 Element 类,那么可以这么处理:

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

// Uses Element from lib1.
Element element1 = Element();

// Uses Element from lib2.
lib2.Element element2 = lib2.Element();

部分导入

如果只想使用代码库的一部分,可以使用部分导入。

// Import only foo.
import 'package:lib1/lib1.dart' show foo;

// Import all names EXCEPT foo.
import 'package:lib2/lib2.dart' hide foo;

export

使用 export 关键字导出

export 'package:lib1/lib1.dart';
export 'src/middleware.dart' show Middleware, createMiddleware;

延迟加载

延迟加载(也常称为 懒加载)是有需要的时候再去加载。

目前只有 dart2js 支持延迟加载 Flutter、Dart VM 以及 DartDevc 目前都不支持延迟加载

使用 deferred as 关键字来标识需要延时加载的代码库

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

当实际需要使用库的 API 时先调用loadLibrary函数加载库:

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

使用 await 关键字暂停代码执行直到库加载完成。loadLibrary 函数可以调用多次也没关系,代码库只会被加载一次。

当你使用延迟加载的时候需要牢记以下几点:

  • 延迟加载的代码库中的常量需要在代码库被加载的时候才会导入,未加载时是不会导入的。
  • 导入文件的时候无法使用延迟加载库中的类型。如果你需要使用类型,则考虑把接口类型转移到另一个库中然后让两个库都分别导入这个接口库。
  • Dart会隐式地将 loadLibrary() 导入到使用了 deferred as 命名空间 的类中。 loadLibrary() 函数返回的是一个 Future。

包管理建议

如果我们有实现自己的代码库,为了提升性能,应该将代码放到/lib/src目录下,然后在/lib目录导出src目录内的 API,实现对 lib/src 目录中 API 的公开。

异步

Future

Future 跟 JavaScript 的Promise 差不多,要使用async和await来让代码变成异步的。必须在带有 async 关键字的 异步函数 中使用 await:

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

上面的代码会等到lookUpVersion处理完成,再执行下一步操作。

await 表达式的返回值通常是一个 Future 对象;如果不是的话也会自动将其包裹在一个 Future 对象里。 Future 对象代表一个“承诺”, await 表达式会阻塞直到需要的对象返回。

async关键字

跟 JavaScript 的规则差不多,单单使用async只能生成 Future 对象,并不会让代码变成异步的。举个🌰

  Future<void> checkVersion() async {
    print(123);
  }
    checkVersion();
  print(456);
    // 123
    // 456

上面的代码并不会让checkVersion变成异步代码,因为 123 在 456 前面打印了。

如果加上 await 就可以对代码进行阻塞,使其变成真正的异步代码。

  Future<int> getVersion() async => 123;

  Future<void> checkVersion() async {
    print(0); //这里还是同步代码
    var res = await getVersion(); //这里开始变成异步
    print(res);
  }

  checkVersion();
  print(456);

上面的结果是

0
456
123

异常处理

使用 try、catch 以及 finally 来处理使用 await 导致的异常:

try {
  version = await lookUpVersion();
} catch (e) {
  // React to inability to look up the version
}

Typedefs

typedefs 是类型别名,是一种引用某一类型的简便方法,常用于封装类型,它使用 typedef 关键字。

比如项目中有一个类型是数字类型的 List,我们将它封装起来变成一个类型别名,就可以直接使用

typedef IntList = List<int>;

IntList a = [1, 2, 3];

当传参是函数并且需要明确的类型定义时,使用类型别名可以简化代码

  void PrintString(String getS(String str)) {
    print(getString('name'));
  }

上面的PrintString函数需要传入一个返回值和参数都为 String 的函数,使用typedef简化代码:

typedef GetString = String Function(String str);

 void PrintString(GetString getS) {
    print(getString('name'));
  }

results matching ""

    No results matching ""