Flutter入门——Dart类与面向对象

Posted by lingjye on July 12, 2019

Dart是一种面向对象的语言,支持基于mixin的继承。每个对象都是一个类的实例,所有类都继承自Object。 基于Mixin的继承意味着虽然每个类(除了Object)只有一个超类,一个类可以继承自多个父类(多继承)。

类的定义

由关键字class类声明一个类,类的命名首字母一般大写,变量名使用驼峰命名规则:

1
2
class Point {
}

构造函数

通过创建与类同名的函数来声明构造函数(另外,命名构造函数中也可以加入一些可选附加标识符)。

1
2
3
4
5
6
7
8
9
class Point {
  // 声明实例变量x,y
  num x ,y;
  // 构造函数 
  Point(num x, num y) {
    this.x = x;
    this.y = y;
  }
}

使用Dart语法糖会更加简单,this关键字指向了当前类的实例:

1
2
// 使用Dart语法糖
Point(this.x, this.y);

默认构造函数

如果未声明构造函数,则提供默认的构造函数,默认构造函数没有参数,并在超类中调用无参数构造函数。这一点类似于C++。

构造函数不能继承

子类不从其父类继承构造函数。没有声明构造函数的子类,系统提供默认(无参数,无名称)构造函数。

命名的构造函数

使用命名构造函数可以从其他类或现有数据中快速实现构造函数:

1
2
3
4
5
6
7
8
9
10
11
class Point {
   num x, y;

   Point(this.x, this.y);

   // 命名构造函数Named constructor
   Point.origin() {
    x = 0;
    y = 0;
  }
}
  • 构造函数不能继承,这意味着超类的命名构造函数不会被子类继承。如果希望使用超类中定义的命名构造函数去创建子类,则必须在子类中实现该构造函数。

使用构造函数

构造函数可以是ClassName或者ClassName.identifier。例如:

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

其中的new关键字是可选的。

有些类提供常量构造函数,将const关键字放在构造函数名称前即可:

1
var p = const ImmutablePoint(2, 2);

构造两个值相同的常量会产生一个相同的实例:

1
2
3
4
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);

assert(identical(a, b)); // They are the same instance!

使用类成员

对象具有由函数和成员数据(分别为方法和实例变量)组成。可以使用点(.)来引用实例变量或方法:

1
2
var p = Point(2, 2);
p.y = 3;

用?表示可能为null

1
p?.y = 4;

获取对象的类型

要在运行时获取对象的类型,可以使用Object的runtimeType属性,该属性返回Type对象:

1
print('a的类型:${a.runtimeType}');

实例化变量

所有未初始化的实例变量都具有该值null:

1
2
3
4
class Point { 
	num x ; //声明实例变量x,初始值为null。
	num y = 0 ; //声明y,初始值为0. 
}  

所有实例变量都隐式生成一个setter方法。非空实例变量也会隐式生成getter方法。

1
2
3
4
5
6
7
8
9
10
11
class Point {
    num x;
    num y;
 }

 main() {
    var point = new Point();
    point.x = 4;          // Use the setter method for x.
    assert(point.x == 4); // Use the getter method for x.
    assert(point.y == null); // Values default to null.
 }

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

默认情况下,子类只能调用父类中未命名的无参数构造函数。父类的构造函数在子类构造函数前被调用。如果还使用initializer list,则先调用initializer list中的内容,然后执行弗雷中的未命名的无参数构造函数,最后调用子类未命名的无参数构造函数。执行顺序如下:

  1. initializer list(初始化列表)
  2. super class’s no-arg constructor(父类的无参数构造函数)
  3. main class’s no-arg constructor(主类的无参数构造函数)

如果父类没有未命名的无参数构造函数,则必须手动调用父类中的一个构造函数。父类构造函数在子类构造函数名后的冒号’:‘之后调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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');
  }
}

其参数还可以是一个表达式:

1
2
3
4
class Employee extends Person {
  Employee() : super.fromJson(getDefaultData());
  // ···
}

父类构造函数的参数无权访问this。例如,参数可以调用静态方法,但不能调用实例方法。

初始化列表

除了调用父类构造函数,还可以在构造函数体运行之前初始化实例变量。变量之间用逗号’,‘分隔。

1
2
3
4
5
Point.fromJson(Map<String, num> json)
  : x = json['x'],
    y = json['y'] {
    print('In Point.fromJson():($x, $y)');
}
  • 初始化程序的右侧不能访问this

重定向构造函数

有时构造函数的目的只是重定向到该类中的另一个构造函数。重定向构造函数的主体是空的,即没有函数体,构造函数的调用放在冒号‘:’之后。

1
2
3
4
5
6
7
class RedirectPoint {
  num x, y;
  // 主题构造函数
  RedirectPoint(this.x, this.y);
  // 重定向构造函数,指向主构造函数,函数体为空
  RedirectPoint.alongXAxis(num x) : this(x, 0);
}

常量构造函数

如果使用该类生成的对象永远不会改变,那么就可以让这些对象成为编译时常量。为此,使用const来定义构造函数,用final来定义所有的实例变量。

1
2
3
4
5
class ImmutablePoint {
    final num x, y;
    const ImmutablePoint(this.x, this.y);
    static final ImmutablePoint origin = const ImmutablePoint(0, 0);
}

工厂构造方法

当实现一个使用factory关键字修饰的构造函数时,这个构造函数不必创建该类的新实例。例如,一个工厂构造方法可能从缓存中返回一个实例,或者它可能返回一个子类的实例。

下面例子从缓存中返回一个实例:

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
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);
    }
  }
}

// 调用工厂构造函数
var logger = Logger('UI');
logger.log('log');
  • 工厂构造方法也不能访问this

方法

方法就是为对象提供行为的函数。

实例方法

对象的实例方法可以访问对象的实例变量和this。

1
2
3
4
5
6
7
8
9
10
11
12
13
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

getter和setter是提供对象属性读写访问权限的特殊方法。每个实例变量都有一个隐式getter,如果合适的话还有一个setter。可以使用get和set关键字来实现getter和setter方法创建其他属性 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Rectangle {
  num left, top, width, height;
  Rectangle(this.left, this.top, this.width, this.height);

  // 定义两个计算属性 righ ,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;
}

// 调用
var rect = dartclasses.Rectangle(2, 4, 20, 15);
rect.right = 12;
print('rect.left:${rect.left}, rect.top:${rect.top}');

抽象方法

实例,getter和setter方法可以是抽象的,定义一个接口,但将其实现留给其他类。抽象方法只能存在于抽象类中。

要使方法成为抽象方法,需要使用分号‘;’,而不是方法体:

1
2
3
4
5
6
7
8
9
10
11
// 抽象方法
abstract class Doer {
  // 定义实例变量和方法,这里定义了一个抽象方法
  void doSomething(); 
}

class EffectiveDoer extends Doer {
  void doSomething() {
    // 提供方法的实现,所以这里的方法不是抽象的
  }
}

抽象类

使用abstract修饰符定义的无法实例化的类称为抽象类。抽象类在定义接口时非常有用,通常还包含了一些实现。如果让抽象类看起来是可实例化的,可以定义工厂构造函数。

抽象类通常包含抽象方法。例如:

1
2
3
4
5
6
7
// 抽象类, 不能被实例化
abstract class AbstractContainer {
  // 定义构造函数,域,方法...

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

隐式接口

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

一个类通过implements子句声明,来实现一个或多个接口,然后提供接口所需的API。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 隐式接口
class ImplicitPerson {
  // 定义私有变量
  final _name;
  // 构造函数
  ImplicitPerson(this._name);
  // 接口
  String greet(String who) => 'Hello, $who, I am $_name';
}

// Person的接口实现
class Impostor implements ImplicitPerson {
  get _name => '';
  String greet(String who) => 'Hi $who, Do you know who I am?';
}

// 调用
String greetBob(dartclasses.ImplicitPerson perspn) => perspn.greet('Bob');
print(greetBob(dartclasses.ImplicitPerson('Kathy')));
print(greetBob(dartclasses.Impostor())); 

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

1
class Point implements Comparable, Location {...}

类的扩展

使用extends创建一个子类,同事supper将指向父类:

1
2
3
4
5
6
7
8
9
10
class Television {
  void turnOn() {
  }
}

class SmartTelevision extends Television {
  void turnOn() {
    super.turnOn();
  }
}

重写成员

使用@override来表明重写一个成员

1
2
3
4
5
6
class SmartTelevision extends Television {
  @override
  void turnOn() {
  	// ...
  }
}
  • 要在类型安全的代码中缩小方法参数或实例变量的类型,可以使用covariant关键字。

可重写的运算符

< + | []
> / ^ []=
<= ~/ & ~
>= * << ==
% >>  
  • !=不是一个可重写的运算符。表达式e1 != e2只是!(e1 == e2)的语法糖。

重写+和-运算符的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 重写运算符
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);
  // Operator == and hashCode not shown.
}

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

  assert(v + w == Vector(4, 5));
  assert(v - w == Vector(0, 1));
}
  • 如果重写==,则还应重写Object的hashCode的getter。

noSuchMethod()

如果使用不存在的方法或实例变量时,可以重写noSuchMethod():

1
2
3
4
5
6
7
8
class A {
  // 除非重写 noSuchMethod,  否者将会抛出 NoSuchMethodError.
  @override
  void noSuchMethod(Invocation invocation) {
    print('You tried to use a non-existent member: ' +
        '${invocation.memberName}');
  }
}

如果不满足以下情况,那么无法调用未实现的方法:

  1. 接收器具有静态类型dynamic;
  2. 接收器有一个静态类型,它定义了未实现的方法(abstract is OK),接收器的动态类型和Object类对象的noSuchMethod()实现不同。

枚举类型

枚举类型(通常称为enumerations或enums)是一种特殊类,用于表示固定数量的常量值。使用关键字enum来声明。

1
enum Color { red, green, blue }

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 枚举
List<dartclasses.Color> colors = dartclasses.Color.values;
print(colors);

var aColor = dartclasses.Color.blue;
switch (aColor) {
	case dartclasses.Color.red:
	  print('Red');
	  break;
	case dartclasses.Color.blue:
	  print('Blue');
	  break;
	case dartclasses.Color.green:
	  print('Green');
	  break;
}

枚举类型具有以下限制:

  1. 不能子类化,mix in或实现枚举。
  2. 无法显式实例化枚举

为类添加特征:mixins

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

要使用 mixin,需要在with关键字后跟一个或多个mixin的名称。

要实现 mixin,要创建一个继承Object的子类,并且不声明构造函数,然后使用mixin关键字来替换掉class。

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
// mixin
mixin Musical {
  bool canPlayPiano = false;
  bool canCompose = false;
  bool canConduct = false;

  void entertainMe() {
    if (canPlayPiano) {
      print('Playing piano');
    } else if (canCompose) {
      print('Waving hands');
    } else {
      print('Humming to self');
    }
  }
}
// Performer类
class Performer {
  // ...
  void doSomething(){
  }
}
// 使用mixin: Musical
class Musician extends Performer with Musical {

}
// mixin
mixin Aggressive {

}
// mixin
mixin Demented {

}
// 使用mixin: Musical, Aggressive, Demented
class Maestro extends Performer
    with Musical, Aggressive, Demented {
  var name;
  bool canConduct;
  Maestro(String maestroName) {
    name = maestroName;
    canConduct = true;
  }
}

Dart 2.1中引入了mixin对关键字的支持,要指定只有某些类型可以使用mixin,你的mixin可以调用它没有定义的方法–使用关键字on来指定所需的超类:

1
2
3
mixin MusicalPerformer on Musician {
  // ···
}

类变量和方法

使用static关键字实现类变量和方法。

  • 静态变量

静态变量对类状态和常量是很有用的,它在使用之前不会被初始化。

1
2
3
4
5
6
7
8
class Queue {
  static const initialCapacity = 16;
  // ···
}

void main() {
  assert(Queue.initialCapacity == 16);
}
  • 静态方法

静态方法不能对实例进行操作,并且不能访问this。

1
2
3
4
5
6
7
8
9
10
11
12
import 'dart:math';

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

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

可调用的类

要允许像函数一样调用Dart类的实例,需要实现call()方法。

在下面的示例中,WannabeFunction类定义了一个call()函数,

1
2
3
4
5
6
7
8
9
class WannabeFunction {
  call(String a, String b, String c) => '$a $b $c!';
}

main() {
  var wf = new WannabeFunction();
  var out = wf("Hi","there,","gang");
  print('$out');
}

本文Demo