Dart 类

2023/5/22

#

简单来说,类就是通过 class 声明的代码段,包含属性和方法。

  • 属性:用来描述类的变量
  • 方法:类中的函数称为类的方法

对象是类的 实例化 结果(var obj = new MyClass()

类与对象的关系:类是对象的抽象,对象是类的具体实现

下面是 Dart 类的简单写法:

// 声明类
class Person {
  // 类的属性
  String name = 'Lawrence';

  // 类的方法
  void getInfo() {
    print('我是$name');
  }
}

void main() {
  // 实例化类,然后得到一个对象
  Person p = new Person();
  // 访问类中的属性
  print(p.name);
  // 访问类的方法
  p.getInfo();

  // Dart 中所有的内容都是对象
  Map m = new Map();
  print(m.length);
  m.addAll({'name': '李四', 'age': 20})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 普通构造函数

声明一个与 类名一样的函数 即可声明一个构造函数。在实例化时,自动被调用。

class Point1 {
  num? x, y;

  // 声明普通构造函数
  Point() {
    print('我是默认构造函数,实例化时会第一个被调用');

    x = 0;
    y = 1;
    // or this.x = 0;
    // or this.y = 1;
  }
}

class Point2 {
  num? x, y;

  // 声明普通构造函数
  Point(num x, num y) {
    // 当命名指向有歧义时,this 不能省略
    this.x = x;
    this.y = y;
  }
}

class Point3 {
  num? x, y;

  // 声明普通构造函数
  // 如果只是单纯为了类属性的赋值,可以使用下面的语法糖
  Point(this.x, this.y)
}

void main() {
  var p1 = new Point1();
  print(p1.x); // 0

  var p2 = new Point2(3, 4);
  print(p2.x); // 3

  var p3 = new Point3(3, 4);
  print(p2.x); // 3
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

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

当且仅当命名冲突时使用 this 关键字才有意义,否则 Dart 会忽略 this 关键字。

# 命名构造函数

可以为一个类声明多个命名构造函数 来表达更明确的意图

class Point {
  num? x, y;

  Point(this.x, this.y);

  // 命名构造函数
  Point.origin() {
    x = 0;
    y = 0;
  }

  // 命名构造函数
  Point.fromJson({x = 0, y = 0}) {
    this.x = x;
    this.y = y;
  }
}

void main() {
  // 默认坐标
  Point p1 = new Point.origin();
  print(p1.x);

  // 手动设置坐标
  Point p2 = new Point.fromJson(x: 2, y: 3);
  pringt(p2.x);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 常量构造函数

如果类生成的对象不会改变,可以通过常量构造函数使这些对象成为编译时常量。

class Point {
  num? x, y;

  Point(this.x, this.y);
}

class ImmutablePoint {
  // 如果声明了常量构造函数,那么类的属性必须通过 final 声明
  final num x;
  final num y;

  // 常量构造函数必须通过 const 声明
  const ImmutablePoint(this.x, this.y);
}

void main() {
  var p1 = new Point(1, 2);
  var p2 = new Point(1, 2);
  print(p1 == p2); // false

  // 常量构造函数,可以当作普通构造函数使用
  var p3 = new ImmutablePoint(1, 2);
  var p4 = new ImmutablePoint(1, 2);
  print(p3 == p4); // false

  // 声明不可变对象,必须通过 const 关键字
  var p5 = const ImmutablePoint(1, 2);
  var p6 = const ImmutablePoint(1, 2);
  print(p5 == p6); // true


  // 实例化时,new 关键字可以省略
  var p7 = ImmutablePoint(1, 2);
  var p8 = ImmutablePoint(1, 2);
  print(p7 == p8); // true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 工厂构造函数

使用 factory 关键字标识类的构造函数将会令该构造函数变为工厂构造函数,这将意味着使用该构造函数构造类的实例时并非总是会返回新的实例对象。工厂函数不会自动生成实例,而是通过代码来决定返回的实例

class Person {
  String? name;

  static Person? instance;

  // 工厂构造函数
  factory Person([String name = 'Lawrence']) {
    // 工厂构造函数中不能使用 this 关键字
    if (Person.instance == null) {
      // 第一次实例化
      Person.instance = new Person.newSelf(name);
    }

    // 非第一次实例化
    return Person.instance!;
  }

  // 命名构造函数
  Person.newSelf(this.name);
}

void main () {
  var p1 = new Person('HCM');
  print(p1.name);

  var p2 = new Person('Ming');
  print(p2.name);

  print(p1 == p2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

上面的代码通过工厂构造函数实现了一个 单例模式

# 访问修饰

Dart 与 TypeScript 不同,没有访问修饰符(public、protected、private)。

  • public:可以被其他所有类访问
  • protected:可以在自身即子类中访问
  • private:只能在自身访问和修改

Dart 类中,默认的访问修饰符是公开的(即 public)

如果属性或方法以 _ (下划线)开头,则表示私有(即 private)

如果声明的私有属性所在类与 main 函数在同一作用域中,则不起作用。只有把类单独抽离出去,私有属性和方法才起作用

// /lib/Person.dart
class Person {
  String? name;

  // 声明私有属性
  num _money = 100;

  Person(this.name);

  num getMoney() {
    // 私有属性只能在当前 类内部 进行访问和修改,在外部只能间接访问和修改
    return _money;
  }

  // 声明私有方法
  void _wife() {
    print('我是 $name 的妻子');
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 引入类
import './lib/Person.dart'

void main() {
  var p = new Person('lawrence');
  print(p.name);

  // 访问私有属性,需要定义一个 public 方法去获取
  print(p.getMoney());

  // 访问私有方法
  print(p._wife()); // Error: The method _wife isn't defined for the type 'Person'.
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# Getter 与 Setter

Getter(获取器)是通过 get 关键字修饰的方法。函数没有小括号,访问时也没有小括号(像访问属性一样访问方法)。

Setter(修改器)是通过 set 关键字修饰的方法。访问时,像设置属性一样给函数传参。

class Circle {
  final double PI = 3.1415;
  num r;

  Circle(this.r);

  // num area() {
  //   return PI * r * r;
  // }

  // 使用 get 声明的方法,不能有小括号
  num get area {
    return PI * r * r;
  }

  // Setter
  set setR(value) {
    this.r = value;
  }
}

void main() {
  var c = new Circle(10);
  print(c.area);

  // 通过 setter 修改属性
  c.r = 20;
  print(c.area);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 初始化列表

初始化列表是在构造函数中 设置属性的默认值,它会在构造函数体执行之前执行。常用于设置 final 常量的值。

class Rect {
  int height;
  int width;

  // 实例化时必须传入两个参数进行初始化
  // Rect(this.height, this.width);

  // 属性为非空类型时,可选位置参数和命名参数可能会传入 null 而报错
  // Rect([int height = 10, int width = 20]) {
  //   this.height = height;
  //   this.width = width;
  // }

  // 初始化列表
  Rect() : height = 10, width = 20 {
    print('${this.height} -- ${this.width}');
  }
}

class Point {
  double x, y, z;

  Point(this.x, this.y, this.z);

  // 初始化列表的特殊用法(重定向构造函数)
  // 这里的 this 指向当前类(可以理解为调用了默认构造函数)
  Point.twoD(double x, double y) : this(x, y, 0);
}

void main() {
  var r = new Rect();
  print(r.height);

  // 实例化点
  var p = new Point(1, 2, 3);
  print(p.z);

  var p2 = new Point.twoD(4, 5);
  print(p2.z);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# static

static 关键字用来指定静态成员,通过 static 修饰的属性是静态属性,通过 static 修饰的方法是静态方法。

静态成员可以通过类名称直接访问(不需要实例化)。实例化是比较消耗资源的,声明静态成员,可以提高程序性能

静态方法不能访问非静态成员,非静态方法可以访问静态成员。静态方法中不能使用 this 关键字(this 指向实例化对象,在静态方法中没有实例化对象),非静态方法中不能使用 this 关键字访问静态属性。

class Person {
  static String name = 'Lawrence';
  int age = 18;

  static printInfo() {
    // print(this.name); 不能通过 this 关键字,访问静态属性
    print(name);

    // print(age); 静态方法不能访问非静态属性

    // printUserInfo(); 静态方法不能访问非静态方法
  }

  printUserInfo() {
    // 非静态方法可以访问静态属性
    print(name);
    print(age);

    // 非静态方法可以访问静态方法
    printInfo();
  }
}

void main() {
  // 静态成员,可以通过类名称直接访问
  print(Person.name);
  Person.printInfo();

  // print(Person.printUserInfo()); 不能通过类名称直接访问非静态方法
  
  var p = new Person();
  // print(p.name); 不能通过实例化对象去访问静态属性
  p.printUserInfo();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 元数据

元数据以 @ 开头,可以给代码标记一些额外的信息。元数据可以用来库、类、构造器、函数、字段、参数或变量声明的前面。

  • @override(重写):某方法添加该注解后,表示重写了父类中的同名方法
  • @required(必填):可以通过 @required 来注解 Dart 中的命名参数,用来指示它是必填参数
  • @deprecated(弃用):若某类或某方法加上该注解之后,表示此方法或类不再建议使用
class Phone {
  // 这是旧版本的开机方法,会在将来的版本中移除
  
  activate() {
    turnOn();
  }

  turnOn() {
    print('开机');
  }
}

void main() {
  var p = new Phone();
  p.activate(); // 'activate' is deprecated and shouldn't be used.

  p.turnOn();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 继承

根据类的先后顺序,可以将类分成父类和子类。

子类可以通过 extends 关键字继承父类。继承后,子类可以使用父类,可见的内容(属性和方法)。

子类中,可以通过 @override 元数据来标记覆写方法。覆写方法就是子类中与父类中同名的方法。

子类中,可以通过 super 关键字来引用父类中,可见的内容,包括属性和方法(普通构造函数,命名构造函数)。

// /lib/Father.dart
class Father {
  String name = 'Lawrence';
  String job;
  num _money = 10000;

  Father(this.job);

  // 命名构造函数
  Father.origin(this.job);

  say() {
    print('我是$name');
  }

  get money {
    return _money;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// /lib/Son.dart
import 'Father.dart';

class Son extends Father {
  String name = 'LawrenceII';

  // 通过 super 继承父类的普通构造函数
  Son(String job) : super(job);

  // 继承命名构造函数
  // Son(String job) : super.origin(job);
  Son.origin(String job) : super.origin(job);

  
  say() {
    super.say();
    print('我是$name, 我爹是${super.name}, 他的工作${super.job}');
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import './lib/Father.dart';
import './lib/Son.dart';

void main() {
  var f = new Father('engineer');
  print(f.name); // Lawrence

  // var s = new Son('engineer');
  var s = new Son.origin('engineer2');
  print(s.name); // LawrenceII
  // print(s._money); 子类不能访问父类中的私有内容
  print(s.money); // 10000
  s.say();
  /*
    我是LawrenceII
    我是LawrenceII, 我爹是Lawrence, 他的工作engineer2
   */
   // 子类有自己的 name 属性,优先获取子类的 name
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 抽象类

抽象类是用 abstract 关键字修饰的类。

抽象类的 作用是充当普通类的模板,约定一些必要的属性和方法。

抽象方法是指 没有方法体的方法

  • 抽象类中一般都有抽象方法,也可以没有抽象方法
  • 普通类中,不能有抽象方法

抽象类不能被实例化(不能被 new)

抽象类可以被普通类继承(extends),如果普通类继承抽象类,必须实现抽象类中所有的抽象方法

抽象类还可以充当接口被实现(implements),如果把抽象类当做接口实现的话,普通类必须得实现抽象类里面定义的所有属性和方法。

// 1. 抽象类,必须通过 abstract 关键字声明
// 2. 抽象类中,可以有抽象方法,也可以没有抽象方法。一般来说,抽象类都有抽象方法
abstract class Phone {
  // 声明抽象方法
  void processor(); // 手机处理器

  void camera(); // 手机摄像头

  void info() {
    print('我是抽象类中的一个普通方法');
  }
}

class Xiaomi extends Phone {
  // 普通类继承了抽象类,就必须实现抽象类中所有的抽象方法
  
  void processor() {
    print('骁龙888');
  }

  
  void camera() {
    print('三星摄像头');
  }

  // 普通类中不能有抽象方法
  // void aaa();
}

class Huawei extends Phone {
  
  void processor() {
    print('麒麟990');
  }

  
  void camera() {
    print('徕卡摄像头');
  }
}

void main() {
  // 抽象类不能被实例化
  // var p1 = new Phone(); Error

  Xiaomi m = new Xiaomi();
  m.processor(); // 骁龙888
  m.camera(); // 三星摄像头
  m.info(); // 我是抽象类中的一个普通方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 接口

接口在 Dart 中就是一个类(只是用法不同)。与 Java 不同,Java 中的接口需要用 interface 关键字声明;Dart 中不需要。接口可以是任意类,但一般使用抽象类做接口

一个类可以 实现(implements)多个接口,多个接口用逗号分隔。class MyClass implements interface1, interface2 {...}

接口可以看成一个个小零件。类实现接口就相当于组装零件。

普通类实现接口后,必须重写接口中所有的属性和方法

// 手机处理器
abstract class Processor {
  String cores = ''; // 内核数

  arch(String name); // 芯片制程
}

// 手机的摄像头
abstract class Camera {
  String resolution = ''; // 分辨率
  
  brand(String name); // 品牌
}

// 通过普通类实现接口
class Phone implements Processor, Camera {
  
  String cores;
  
  
  String resolution;

  Phone(this.cores, this.resolution);

  
  arch(String name) {
    print('芯片制成$name');
  }

  
  brand(String name) {
    print('相机品牌$name');
  }
}

void main() {
  Phone p = new Phone('4核', '4800万');

  p.arch('5nm');
  p.brand('索尼');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 混入

混入(Mixin)是一段公共代码。混入有两种声明方式:

  • 将类当作混入 class MixinA {...}
    • 作为 Mixin 的类只能继承自 Object,不能继承其他类
    • 作为 Mixin 的类不能有构造函数
  • 使用 mixin 关键字声明 mixin MixinB {...}

混入(Mixin)可以提高代码的复用的效率,普通类可以通过 with 来使用混入。

使用多个混入时,后引入的混入会覆盖之前混入中的重复的内容。比如,MixinA 和 MixinB 中都有 hello() 方法,MyClass 会使用 MixinB 中的。

class Father {}

// class MixinA extends Father 用作混入的类不能继承除了 Object 以外的其他类
// class MixinA extends Object

class MixinA {
  String name = 'MixinA';

  // MixinA(this.name); 用作混入的类不能拥有构造函数

  void printA() {
    print('A');
  }

  void run() {
    print('A is running');
  }
}

// 如果要使用混入,最好还是使用 mixin 关键字显式声明
mixin MixinB {
  String name = 'MixinB';

  void printB() {
    print('B');
  }

  void run() {
    print('B is running');
  }
}

class MyClass with MixinA, MixinB {}

void main() {
  var c = new MyClass();

  c.printA();
  c.printB();

  // 后引入的混入会覆盖之前引入的混入中的重复内容
  print(c.name);
  c.run();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44