Dart基础之Classes(类)上篇

2,918 阅读4分钟

前言

Dart是一种面向对象的语言,具有类和混合继承。 每个对象都是一个类的实例,所有类都来自Object。 混合继承意味着虽然每个类(除了Object)只有一个超类(也称为父类),但是类体可以在多个类层次结构中重用。

如何使用类成员

对象拥有由函数和数据(分别为方法和实例变量)组成的成员。在一个对象上调用方法时:该方法可以访问该对象的函数和数据。

用逗号(.)去获取一个实例的变量或者方法:

var p = Point(2, 2);

// 设置实例 p 的 属性 y 为 3
p.y = 3;

// 获取实例 p 的属性 y
assert(p.y == 3);

// 调用实例 p 的 distanceTo()
num distance = p.distanceTo(Point(4, 4));

当最左边的操作数为null时,请用?.来代替.以避免异常发生

// 当 p 为非空时, 才会把 y 设置为 4
p?.y = 4;

利用构造函数

你可以用构造函数来创建一个对象, 构造函数的名字可以是 ClassName 或者 ClassName.identifier。例如,下面的代码用Point()Point.fromJson()构造函数创建Point对象:

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

下面的代码有相同的作用,但是在构造函数名字前用了一个可选的关键字new:

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

注意版本:关键字new在Dart2中变为可选的

一些类提供了常量构造函数。为了创建一个编译时的常量可以使用常量构造函数,把const关键字放在构造函数名称前就可以了:

var p = const ImmutablePoint(2, 2);

构建两个相同的编译时常量会产生一个单个,标准的实例:

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

assert(identical(a, b)); // a和b是同一个实例

在一个常量上下文中,你可以在构造函数或者文字前省略const。例如,看下下面的代码,创建了一个map常量:

// 茫茫多的 const 关键字
const pointAndLine = const {
  'point': const [const ImmutablePoint(0, 0)],
  'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};

你可以省略除了第一个的其他const关键字:

// 只要一个 const 就可以建立常量上下文
const pointAndLine = {
  'point': [ImmutablePoint(0, 0)],
  'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};

如果一个常量构造函数在常量上下文的外面,并且没有const调用, 它会创建一个非常量的对象:

var a = const ImmutablePoint(1, 1); // 创建一个常量
var b = ImmutablePoint(1, 1); // 没有创建一个常量

assert(!identical(a, b)); // 不是同一个实例

注意版本:关键字 const在 Dart2 的常量上下文中才变可选

获取一个对象的类型

在运行时获取一个对象的类型,你可以使用ObjectruntimeType属性,它会返回一个Type对象。

print('The type of a is ${a.runtimeType}');

看到这里,你已经知道怎么去使用classes了。剩下的章节教你如何去实现classes

实例变量

class Point {
  num x; //申明一个实例变量x, 初始值null
  num y; //申明一个y,初始值null
  num z = 0; //申明一个z,初始值0 
}

所有没有初始化过的实例变量,默认值都是 null

所有实例变量会生成一个隐式的的getter方法。所有的非 final 定义的实例变量会生成一个隐式的setter方法。

class Point {
  num x;
  num y;
}

void main() {
  var point = Point();
  point.x = 4; // 使用了 x 的 `setter` 方法.
  assert(point.x == 4); // 使用了 x 的 `getter` 方法.
  assert(point.y == null); // y 没有赋值的情况下默认 `getter` 方法拿到的值是 `null`.
}

如果你在申明时初始化了一个实例变量(代替了在构造函数或者方法中),当实例被创建时就会被赋值,相当于在构造函数和它的初始化列表执行之前。

构造函数

创建一个跟类同名的函数来申明一个构造函数(增加,可选,一个额外的identifier称为命名构造函数)。下面是构造函数最普通的模式,制造型的构造函数,用来创建一个类的实例:

class Point {
  num x, y;

  Point(num x, num y) {
    // 有更好的方式实现这种赋值
    this.x = x;
    this.y = y;
  }
}

this关键字指的是当前的实例。

注意:只有当存在一个命名冲突时使用this,另外Dart的风格是省略this

通过构造函数给实例变量赋值的方式非常普遍,所以 Dart 有语法糖让它变得更简单:

class Point {
  num x, y;

  // 在构造函数方法体中实现变量的赋值
  Point(this.x, this.y);
}

默认构造函数

即使你不申明一个构造函数,Dart 会默认提供一个给你。默认构造函数没有参数并且会触发超类(父类)的无参构造函数。

构造函数不能被继承

子类不能继承它们超类的构造函数。一个子类申明无参构造函数只能是默认的(无参数,无命名)构造函数。

命名构造函数

用一个命名构造函数实现多个构造函数,来为一个类提供更清晰的表达:

class Point {
  num x, y;

  Point(this.x, this.y);

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

请记住,构造函数不是继承的,这意味着超类的命名构造函数不会被子类继承。 如果希望使用超类中定义的命名构造函数创建子类,则必须在子类中实现该构造函数。

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

默认情况下,子类中的构造函数调用超类的未命名的无参数构造函数。 超类的构造函数会在构造函数体的开头被调用。 如果还使用初始化列表,则在调用超类之前执行。 总之,执行顺序如下:

  • 初始化列表
  • 超类的无参数构造函数
  • 主类的无参数构造函数

如果超类没有未命名的无参数构造函数,则必须手动调用超类中的一个构造函数。 在冒号(:)之后指定超类构造函数,就在构造函数体(如果有)之前。

在下面的示例中,Employee类的构造函数为其超类Person调用命名构造函数。

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

main() {
  var emp = new Employee.fromJson({});

  // 输出:
  // in Person
  // in Employee
  if (emp is Person) {
    // Type check
    emp.firstName = 'Bob';
  }
  (emp as Person).firstName = 'Bob';
}

因为在调用构造函数之前会计算超类构造函数的参数,所以参数可以是一个表达式,例如函数调用:

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

警告:超类构造函数的参数无权访问它。 例如,参数可以调用静态方法,但不能调用实例方法。

初始化列表

除了调用超类构造函数之外,还可以在构造函数体运行之前初始化实例变量。 用逗号分隔初始化程序。

// 初始化列表会在构造函数体运行之前设置实例变量
Point.fromJson(Map<String, num> json)
    : x = json['x'],
      y = json['y'] {
  print('In Point.fromJson(): ($x, $y)');
}

警告:初始化程序的右侧无权访问this

在开发期间,你可以使用初始化列表中的assert来验证输入。

Point.withAssert(this.x, this.y) : assert(x >= 0) {
  print('In Point.withAssert(): ($x, $y)');
}

初始化列表时可以很方便设置final字段,以下示例初始化初始化列表中的三个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);
}

main() {
  var p = new Point(2, 3);
  print(p.distanceFromOrigin);
}

重定向构造函数

有时构造函数的唯一目的是重定向到同一个类中的另一个构造函数。 重定向构造函数的主体是空的,构造函数调用出现在冒号(:)之后。

class Point {
  num x, y;

  // 这个类的主构造函数
  Point(this.x, this.y);

  // 委托给了主构造函数
  Point.alongXAxis(num x) : this(x, 0);
}

常量构造函数

如果你的类生成对象后永远不会更改,那么可以使这些对象成为编译时常量。 为此,请定义const构造函数并确保所有实例变量都是final

class ImmutablePoint {
  static final ImmutablePoint origin =
      const ImmutablePoint(0, 0);

  final num x, y;

  const ImmutablePoint(this.x, this.y);
}

工厂构造函数

在实现并不总是创建一个类新实例的构造函数时,请使用factory关键字。 例如,工厂构造函数可能从缓存中返回实例,或者它可能返回子类型的实例。

以下示例演示了从缓存中返回对象的工厂构造函数:

class Logger {
  final String name;
  bool mute = false;

  // _cache 是库私有的
  static final Map<String, Logger> _cache =
      <String, Logger>{};

  factory Logger(String name) {
    return _cache.putIfAbsent(
        name, () => Logger._internal(name));
  }

  Logger._internal(this.name);

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

注意:工厂构造函数没有权限访问this

像调用任何其他构造函数一样调用工厂构造函数:

var logger = Logger('UI');
logger.log('Button clicked');