愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

Dart学习笔记(五):类

Dart是面向对象的语言,所有对象都是一个类的实例,所有类都继承自Object。此外,Dart支持基于mixin的继承机制,这意味着,每个类(除了Object)都只有一个父类,一个类的代码可以在其他多个类继承中重复使用。

一、类的实例化

类的实例化,可以使用new关键字,后面可以接上构造函数(如ClassName或者ClassName.identifier),如:

var jsonData = JSON.decode('{"x":1, "y":2}');
// 直接接 ClassName
var p1 = new Point(2, 2);
// 接 ClassName.identifier
var p2 = new Point.fromJson(jsonData);

如果要访问对象的成员(属性或者方法),可以使用.语法:

var distance = p1.distanceTo(p2);

还可以使用?.来避免左边对象为null时抛出异常:

p?.y = 4; // 如果p为非null,才会执行 p.y = 4;

有些类提供了常量构造函数,可以创造编译时常量,那么这种构造函数的实例化不采用new关键字,而是采用const关键字,如下:

var p = const ImmutablePoint(2, 2);

两个一样的编译时常量是同一个对象,用identical测试返回true

var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);

identical(a, b); // true

此外,runtimeType可以获得一个示例的类型,该属性返回一个Type对象:

main() {
  int a = 1;
  print('type of a is ${a.runtimeType}');
}
// 输出:type of a is int


二、类的定义

使用class关键字可以声明一个类:

class Foo {
}

1)实例变量

属于类的实例的变量称为实例变量实例变量使用var propertyName;或者type propertyName声明在class体里,如果一个实例变量没有赋予初始值,则它的初始值是null,如:

class Point {
    num x; // 初始值 null
    num y; // 初始值 null
    num z;
}

每个实例变量都会隐含地生成一个getter方法,对于非final的变量还会隐含地生成一个setter,所以可以:

var point = new Point();
point.x = 4; // 调用了 setter 方法来设置变量值
point.y; // 调用了 getter 方法来获取变量值

注意: 如果在定义实例变量时初始化了实例变量,那么实例变量的值是在实例创建时、构造函数和初始化参数列表 执行前 初始化的

2)构造函数

class中命名和类名一致的那个函数是构造函数,构造函数会在被实例化时调用,如:

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

其中this关键字指向当前的实例。不过,只有当名字冲突时才使用thisDart中在类里是可以忽略this的,也就是说,我们可以这么写:

class Point {
    num x;
    num y;
    Point(num _x, num _y) {
        x = _x;
        y = _y;
    }
}

或者,也可以直接在构造函数中使用初始化赋值的语法糖:

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

1、默认构造函数

如果在一个类中没有定义构造函数,则会有个默认的构造函数。默认的构造函数无参数,且会调用没有参数的构造函数。

2、构造函数不会继承

子类不会继承父类的构造函数(除非是无名无参的构造函数)

3、命名构造函数

使用命名构造函数可以实现为一个类指定多个构造函数,也可以通过这来更清晰地表达意图,如:

class Point {
    num x;
    num y;
    Point(this.x, this.y);
    Point.fromJson(Map json) {
        x = json['x'];
        y = json['y'];
    }
}

4、调用父类的构造函数

默认情况下,子类的构造函数会自动调用父类的无名无参的默认构造函数,父类的构造函数在子类构造函数的函数体开头位置调用。但是如果提供了初始化参数列表,则初始化参数列表会在父类构造函数之前执行,也就是说,执行顺序如下:

  • 初始化参数列表
  • 父类无名构造函数
  • 子类无名构造函数

如果超类中没有无名无参构造函数,那么就需要手动调用了,调用方法为在构造函数之后使用:调用,如:

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

class Point3D extends Point {
  num z;
  Point3D(num x, num y, num z): super(x, y) {
    this.z = z;
  }
}

由于父类构造函数是在子类构造函数执行前执行的,所以参数可以是一个表达式或者一个方法调用,如:

class A {
    A(String str) {
        print(str);
    }
}
class B extends A {
    B(): super(getDefaultData());
    static getDefaultData() {
        return 'Hello, world';
    }
}

注意:如果是一个类中的方法调用,那么只能使用静态方法

5、初始化列表

初始化列表除了可以调用父类构造函数,还可以初始化实例参数,采用,分隔表达式,如:

class Point {
    num x;
    num y;
    Point(this.x, this.y);
    Point.fromJson(Map jsonMap):
        x = jsonMap['x'],
        y = jsonMap['y'] {
        print('($x, $y)');
    }
}

需要注意的是:初始化表达式=右边的部分不能访问this,此外,对于final变量的值,可以在初始化列表中指定,如:

import 'dart:math';

class Point {
    final num x;
    final num y;
    final num distanceFromOrigin;
    Point(x, y):
        x = x,
        y = y,
        distanceFromOrigin = sqrt(x * x + y * y);
}

6、重定向构造函数

有时候一个构造函数会调用类中的其他构造函数,这种构造函数称之为重定向构造函数,如下:

class Point {
    num x;
    num y;
    Point(this.x, this.y);
    Point.alongXAxis(num x): this(x, 0);
}

7、常量构造函数

如果类提供的是状态不变的对象,那么可以把这些对象定义为编译时常量,实现这种功能可以定义const构造函数,且声明所有类的变量为final,如:

class ImmutablePoint {
    final num x;
    final num y;
    const ImmutablePoint(this.x, this.y);
    static final ImmutablePoint origin = const Immutable(0, 0);
}

8、工厂方法构造函数

如果一个构造函数并不总是返回一个新的对象,那么可以在构造函数前面加上factory关键字,来表示它是一个工厂方法构造函数。但是需要注意的是,工厂构造函数中不能访问this,如下:

class Logger {
    final String name;
    bool mute = false;
    // 缓存实例
    static final Map<String, Logger> _cache = <String, Logger>{};

    factory Logger(String name) {
        if (_cache.containsKey(name)) {
            return _cache[name];
        } else {
            final logger = new Logger._internal(name);
            _cache[name] = logger;
            return logger;
        }
    }

    Logger._internal(this.name);

    void log(String msg) {
        if (!mute) {
            print(msg);
        }
    }
}

那么,在使用new关键词实例化Logger时,每次都会调用factory Logger(String name)这个构造函数:

var logger1 = new Logger('UI');
var logger2 = new Logger('UI');
identical(logger1, logger2); // true

3)实例方法

实例方法可以访问thisDart中实例方法的声明如下:

class A {
    methodName() {
        // ...
    }
}

4)setter/getter

在一个方法前面加上settergetter关键字,那么这个方法就成为了settergetter,就可以对instance.methodName进行赋值或者取值操作,而无需使用()来调用,如:

class Person {
    String firstName;
    String lastName;
    Person(this.firstName, this.lastName);

    String get fullName {
        return '$firstName $lastName';
    }
}
final me = new Person('Ruphi', 'Lau');
print(me.fullName); // 输出:Ruphi Lau

注意,getter里不能带(),即get name()是错误的,get name才是正确的,而setter里则可以接受一个参数,即为赋值传入的值:set name(String name)


三、操作符重写

Dart中支持操作符重写,可被重写的操作符有:

  • 比较运算符:><<=>===
  • 算数运算符:+-*/%~/
  • 位运算符:|&^~<<>>
  • 方括号运算符:[][]=

重写运算符的语法为使用operator关键字紧接运算符,以下例子为实现向量的运算:

class Vector {
    final int x;
    final int y;
    const Vector(this.x, this.y);

    Vector operator +(Vector v) {
        return new Vector(x + v.x, y + v.y);
    }

    Vector operator -(Vector v) {
        return new Vector(x - v.x, y - v.y);
    }
}

main() {
    final v = new Vector(2, 3);
    final w = new Vector(2, 2);
    
    final addRes = v + w;
    print('(${addRes.x}, ${addRes.y})'); // 输出:(4, 5)
    
    final minusRes = v - w;
    print('(${minusRes.x}, ${minusRes.y})'); // 输出:(0, 1)
}


四、抽象类

1)抽象类的定义

不能被实例化的类是抽象类,抽象类通常用来定义接口及部分实现。如果抽象类要被实例化,则需要定义一个工厂构造函数。抽象类的声明使用abstract修饰符:

abstract class AbstractContainer {
    // ...
}

2)抽象函数

抽象函数是之定义函数接口但是没有实现(方法体)的函数,抽象函数由子类实现,调用一个未实现的抽象函数会导致运行时异常。如下:

abstract class Doer {
    void doSomething(); // 抽象函数,没有方法体
}
class EffectiveDoer extends Doer {
    void doSomething() {
        // 在子类中实现
    }
}

3)隐式接口

每个类都隐式地定义了一个包含所有实例成员的接口,并且这个类实现了该接口。Dart中并没有直接提供interface这样子的关键字,因此定义interface应该通过定义一个类实现,如果只想支持某个类的接口但是不想继承它的实现,那么使用implements关键字即可,如下:

class Person {
    final name;
    Person(this.name); // 构造函数不会创建接口
    String greet(who) => 'Hello, $who. I am $name'; // 包含了 greet 的实现
}

class Stark implements Person {
    final name = 'Tony Stark';
    String greet(who) => '$who, I am, I am $name';
}

main() {
    final ironMan = new Stark();
    print(
      ironMan.greet('Thanos')
    );
}

接口是可以多实现的,如下:

class TonyStart implements American, Scientist, Richman, Playboy {
    // ...
}


五、类的继承

类的继承,采用extends关键字,而子类中可以使用supper来引用父类,如下:

class TV {
    void turnOn() {
        _illuminateDisplay();
        _activateIrSensor();
    }
    // ...
}

class SmartTV extends TV {
    void turnOn() {
        super.turnOn();
        _bootNetworkInterface();
        _initializeMemory();
        _upgradeApps();
    }
    // ...
}

子类可以覆写实例函数gettersetter。以下例子为覆写noSuchMethod()函数(这个函数为Object类中当调用了对象上不存在的函数所触发的):

class A {
    void noSuchMethod(Invocation mirror) {
        print('You tried to use a non-existent member: ${mirror.memberName}');
    }
}

此外,可以使用@override注解来表明是想要覆写超类的一个函数,如:

class A {
    @override
    void noSuchMethod(Invocation mirror) {
        // ...
    }
}


六、枚举类型

枚举类型是一种特殊的,用来表示枚举,使用enum关键字可以定义枚举:

enum Color {
    RED,
    GREEN,
    BLUE
}

枚举类型中的每个值都有一个index getter,返回枚举值在定义中出现的位置(从0开始):

Color.RED;   // 0
Color.GREEN; // 1
Color.BLUE;  // 2

可以使用values来获得所有的枚举值,如:

List<Color> colors = Color.values;

若是在switch语句中使用枚举,那么需要处理枚举类型的所有值,或者定义一个default分支,否则会导致抛出一个警告:

Color someColor = Color.RED;
switch (someColor) {
    case Color.RED:
        // ...
        break;
    case Color.GREEN:
        // ...
        break;
    // 会报错,因为没有对 Color.BLUE 进行处理
}

Dart中的枚举类型,有如下的限制:

  • 无法继承枚举类型,无法使用mixin,无法实例化枚举
  • 无法显示地初始化一个枚举类型


七、Mixins

Mixins是一种在多类继承中重用一个类代码的手段,可以为类添加新的功能。使用Mixins的方法为使用with关键字,如下:

class Person {
    final name;
    Person(this.name);
}
class Program {
    program() => print('Program');
}
class Reading {
    reading() => print('Reading');
}
class Tom extends Person with Program, Reading {
    Tom(): super('Tom') {
        print('$name can:');
        program();
        reading();
    }
}

main() {
    new Tom();
}

以上代码输出:

Tom can:
Program
Reading

如果一个类继承Object,但是该类没有构造函数,那么就不能调用super,这个类就是一个mixin,如:

abstract class Musical {
    bool canPlayPiano = false;
    bool canCompose = false;
    bool canConduct = false;

    void entertainMe() {
        if (canPlayPiano) {
            print('Playing piano');
        } else if (canConduct) {
            print('Waving hands');
        } else {
            print('Humming to self');
        }
    }
}

Dart 1.13开始,Dart中的Mixins以下限制不再那么严格了:

  • Mixins可以调用其他类,不再限制为继承Object
  • Mixin可以调用super()


八、静态变量与静态函数

可以使用static关键字来定义静态变量静态函数,他们属于类自身,不属于任意一个实例。如下:

class Chinese {
    static const from = 'China';
    static whereAreYouFrom() {
        print('I am from $from');
    }
}

需要注意的是:

  • 静态方法由于属于,但是this代指的是实例对象,所以静态方法不能访问this
  • 静态方法可以访问其他静态方法静态变量