Dart之旅07: 类

Dart是面向对象语言,它是单继承的。所有类派生自Object

使用类成员

类成员和java一样,分为成员方法和成员属性。使用.或者?.来访问成员:

var p = Point(2, 2);

// Set the value of the instance variable y.
p.y = 3;

// Get the value of y.
assert(p.y == 3);

// Invoke distanceTo() on p.
num distance = p.distanceTo(Point(4, 4));

// If p is non-null, set its y value to 4.
p?.y = 4;

使用构造函数

使用构造函数可以使用类似C++一样的类名,或者像Java的静态工厂方法那样使用ClassName.identifier来创建:

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

上面的代码等效于这段:

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

在Dart2的时候new关键字变成了可选关键字。

一些类提供了常量构造函数,来创建一个编译时常量对象:

var p = const ImmutablePoint(2, 2);

创建相等的编译时常量对象可以使用会生成相通的对象实例,这和Java的字符串常量类似:

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

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

如果实在常量上下文中,你可以省略const,例如如下字面量的声明:

// Lots of const keywords here.
const pointAndLine = const {
  'point': const [const ImmutablePoint(0, 0)],
  'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};

你可以省略这些const

// Only one const, which establishes the constant context.
const pointAndLine = {
  'point': [ImmutablePoint(0, 0)],
  'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};

获取一个对象类型

获取一个对象的运行时类型可以使用runtimeType属性。

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

到此为止,你已经看到了如何使用类,剩下的部分是如何实现一个类。

实例变量

这里是你如何声明一个变量类型:

class Point {
  num x; // Declare instance variable x, initially null.
  num y; // Declare y, initially null.
  num z = 0; // Declare z, initially 0.
}

所有的未初始化变量都是0

所有的实例变量都会生成一个隐含的getter方法。Non-final的实例变量也会生成一个隐含的setter方法,这和kotlin一样。

class Point {
  num x;
  num y;
}

void main() {
  var point = 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.
}

当你定义在实例变量的声明处初始化这个变量的话,这个值会在实例创建之后就初始化,这个初始化时机处于构造函数和初始化列表之前。

构造函数

Dart可以使用Java风格的构造函数:

class Point {
  num x, y;

  Point(num x, num y) {
    // There's a better way to do this, stay tuned.
    this.x = x;
    this.y = y;
  }
}

这里的this关键字和java的用法也是一样的。这里需要显式使用this也是因为参数名和实例变量名的命名冲突。

使用构造函数来初始化变量是一种十分常见的写法,所以dart有一个关于这种写法的语法糖:

class Point {
  num x, y;

  // Syntactic sugar for setting x and y
  // before the constructor body runs.
  Point(this.x, this.y);
}

默认构造函数

如果你不声明构造函数,一个默认的构造函数会提供给你,这个默认的构造函数没有参数,并且默认调用父类的无参构造方法。

构造函数是不继承的

子类不会继承父类的构造方法。一个子类如果没有声明构造方法,那么它只有一个默认构造函数。

命名构造函数

使用命名构造函数来实现多重构造为一个类提供了更加明确的构造方式:

class Point {
  num x, y;

  Point(this.x, this.y);

  // Named constructor
  Point.origin() {
    x = 0;
    y = 0;
  }
}

要记住构造函数不会被继承,如果你想让子类也能使用命名构造函数,你必须自己在子类中实现这个构造函数。

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

默认情况下子类的构造方法会调用父类的非命名无参构造函数,父类的构造函数会在初始化最开始调用(类似java的super第一行原则)。如果有初始化列表的话,那么调用顺序是这样的:

  1. 初始化列表
  2. 父类无参构造函数
  3. 当前类无参构造函数

如果父类没有一个非命名无参构造函数,那么你必须手动调用它的一个构造函数。指定父类构造函数在构造函数之后放上一个冒号(:),并且在构造函数体之前调用

下面的例子就是一个调用父类命名构造函数的代码:

class Person {
  String firstName;

  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  // Person does not have a default constructor;
  // you must call super.fromJson(data).
  Employee.fromJson(Map data) : super.fromJson(data) {
    print('in Employee');
  }
}

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

  // Prints:
  // 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());
  // ···
}

这时的getDefaultData()不能使用this指针,因为它是在父类的构造函数中执行的。

初始化列表

除了调用父类的构造函数,你也可以在初始化列表里面初始化实例变量:

// Initializer list sets instance variables before
// the constructor body runs.
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字段很方便:

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(4, 3);
  print(p.distanceFromOrigin);
}

重定向构造函数

有时一个构造函数仅仅是为了调用另一个构造函数(在Java里面叫做构造函数重载)时,可以使用冒号调用:

class Point {
  num x, y;

  // The main constructor for this class.
  Point(this.x, this.y);

  // Delegates to the main constructor.
  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 is library-private, thanks to
  // the _ in front of its name.
  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);
  }
}

注意:工厂构造函数不能使用this

使用工厂构造函数的方式和正常构造函数一样:

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

方法

类的方法表示这个对象拥有的行为。

实例方法

实例方法可以访问this

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

Getter和Setter

Getter和Setter在编程语言中通常用来给一个类属性提供读写功能。还记得一个类的属性隐含了Getter方法了么,如果没有final修饰还会有隐含的setter方法。你也可以通过getset来实现其他属性。

class Rectangle {
  num left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  // Define two calculated properties: right and 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;
}

void main() {
  var rect = Rectangle(3, 4, 20, 15);
  assert(rect.left == 3);
  rect.right = 12;
  assert(rect.left == -8);
}

通过使用getter和setter你可以封装属性的访问逻辑,而不需要更改客户端代码。

注意:类似于自增++运算符,这些运算符都会以它们的正常方式工作,无论是否明确定义了getter。但为了避免一些副作用,dart在运行时会产生一些隐含的临时变量,而不是对真正的类属性进行运算。

抽象方法

实例,getter和setter都可以是抽象的,抽象方法只定义方法,而不考虑实现。抽象方法只存在于抽象类中。抽象方法只需以分号结尾,而不是方法体。

abstract class Doer {
  // Define instance variables and methods...

  void doSomething(); // Define an abstract method.
}

class EffectiveDoer extends Doer {
  void doSomething() {
    // Provide an implementation, so the method is not abstract here...
  }
}

抽象类

抽象类不能被实例化,他们通常含有抽象方法用来定义一些接口。如果你想让你的抽象类可以实例化,那么就定义一个工厂构造方法吧。(这其实是设计模式中的抽象工厂模式的语法糖,读者可以查阅相关资料深入体会这种设计模式的精妙之处)

下面是一个抽象类的例子:

// This class is declared abstract and thus
// can't be instantiated.
abstract class AbstractContainer {
  // Define constructors, fields, methods...

  void updateChildren(); // Abstract method.
}

隐式接口

每一个类都可以定义为一个隐式接口,如果你想要把一个类当作接口,只使用里面的接口定义,而清除它的实现,那么可以使用implements关键字把一个类当作接口一样实现。这时你需要重写所有的接口方法:

class Person {
  final _name;

  Person(this._name);

  String greet(String who) => 'Hello, $who. I am $_name.';
}

class Impostor implements Person {
  @override
  get _name => null;

  @override
  String greet(String who) => 'Hi $who. Do you know who I am?';
}

String greetBob(Person person) => person.greet('Bob');

void main() {
  print(greetBob(Person('Kathy')));
  print(greetBob(Impostor()));
}

隐式接口可以多重继承:

class Point implements Comparable, Location {...}

继承类

使用extends关键字来继承一个类,并且super关键字来指定父类:

class Television {
  void turnOn() {
    _illuminateDisplay();
    _activateIrSensor();
  }
  // ···
}

class SmartTelevision extends Television {
  void turnOn() {
    super.turnOn();
    _bootNetworkInterface();
    _initializeMemory();
    _upgradeApps();
  }
  // ···
}

重写成员

子类可以重写实例方法,getter和setter。你可以使用@override注解来指明你的重写意图:

class SmartTelevision extends Television {
  @override
  void turnOn() {...}
  // ···
}

关于重写方法,有时子类试图缩小父类方法的类型范围,这时可以使用covariant关键字。更多详情点这里。还有类型安全。这个关键字其实并不常用,因为它还会涉及到一些类型安全问题。除非你有十分合理的理由要违背面向对象的基本原则(里氏替换原则)进行代码设计,否则不必考虑此关键字。

可重写运算符

你可以重写下表中列出来的操作符。例如,如果你定义类一个向量类,那么你可以重写+运算符来进行向量的加法运算

< + | []
> / ^ []=
<= ~/ & ~
>= * << ==
- % >>

你可能发现了!=并不能重写,因为e1 != e2只是一个语法糖for !(e1 == e2)

下面是一个重写+-的例子:

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. For details, see note below.
  // ···
}

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

  assert(v + w == Vector(4, 5));
  assert(v - w == Vector(0, 1));
}

如果你重写了一个类的==运算符,那么你也要重写hashCode属性的getter方法。这类似于Java的equals()hashCode()方法同时重写原则。它会在一些标准API中体现它的价值,比如map的key的实现

noSuchMethod()

要在代码试图使用不存在的方法或实例变量时检测或做出反应,可以重写noSuchMethod():

class A {
  // Unless you override noSuchMethod, using a
  // non-existent member results in a NoSuchMethodError.
  @override
  void noSuchMethod(Invocation invocation) {
    print('You tried to use a non-existent member: ' +
        '${invocation.memberName}');
  }
}

这里我刚看的时候感觉比较鸡肋,如果一个类的对象没有这个方法,那编辑器不就会提示了么?但存在就有存在的理由,你可以调用一个类的不存在的方法有以下几种情况:

  • 方法的接受者是静态类型dynamic
  • 接受者的静态类型定义了一个未实现的方法(比如抽象方法),并且接受者的动态类型实现了与Object不同的noSuchMethod()方法。

详细部分可以点击这里
有关第二种情况,这里根据前文的代码写了一个直观的程序,他可以正常运行:

class Person {
  final _name;

  Person(this._name);

  String greet(String who) => 'Hello, $who. I am $_name.';
}

class Impostor implements Person {
  @override
  get _name => null;

//  @override
//  String greet(String who) => 'Hi $who. Do you know who I am?';

  @override
  void noSuchMethod(Invocation invocation) {
    print('You tried to use a non-existent member: ' +
        '${invocation.memberName}');
  }
}

String greetBob(Person person) => person.greet('Bob');

void main() {
  print(greetBob(Person('Kathy')));
  print(greetBob(Impostor()));
}

枚举类型

枚举类型通常被叫做enumerations或者enums,它是一种固定的类用来表示固定数量的常量值。

使用枚举

声明枚举类:

enum Color { red, green, blue }

每个枚举值有一个index的getter。它返回了这个值在枚举类中的索引:

assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);

和java类似,可以使用values属性获取枚举列表:

List<Color> colors = Color.values;
assert(colors[2] == Color.blue);

switch子句中使用枚举,如果全部枚举值都有case子句,那么不需要default,否则会报警告:

var aColor = Color.blue;

switch (aColor) {
  case Color.red:
    print('Red as roses!');
    break;
  case Color.green:
    print('Green as grass!');
    break;
  default: // Without this, you see a WARNING.
    print(aColor); // 'Color.blue'
}

枚举类型有以下限制:

  • 你不能继承或实现一个枚举,也不能创建枚举类型的子类
  • 你不能显式的实例化枚举对象。

更详细的枚举用法参考dart语言规范

给一个类添加特性:混合

混合(Mixins)是一种在多类层级中重用代码的一种方式。
为了使用混合,使用with关键字跟上这个类要混合特性的混合名字,例如:

class Musician extends Performer with Musical {
  // ···
}

class Maestro extends Person
    with Musical, Aggressive, Demented {
  Maestro(String maestroName) {
    name = maestroName;
    canConduct = true;
  }
}

要想实现一个mixin,创建一个继承Object的类,并且没有构造函数。除非你想创建一个通常的类,否则使用mixin关键字来代替class。例如:

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

混合可以限定能混合此类的目标类,使用on来指定可以混合的类,这样的mixin类型可以使用on指定类型的方法:

mixin MusicalPerformer on Musician {
  // ···
}

mixin这里其实是一种符合面向对象中接口隔离原则的体现。针对含有特定行为的类可以定制特定的方法。在Java代码中的接口隔离早已屡见不鲜并且还有特定的语法支持,比如针对Closeabletry-with-resource语法,和针对Iterable的foreach循环。还有TreeSet中对Comparable的自动排序等等。mixin则可以定义更据针对性的隔离接口,这点算是一个语法层级的优化。

dart从2.1版本开始支持mixin。更多信息查看mixin规范

类变量和类方法

使用static关键字可以创建类变量和类方法

静态变量

静态变量(也叫类变量)是一个类层级的状态和常量:

class Queue {
  static const initialCapacity = 16;
  // ···
}

void main() {
  assert(Queue.initialCapacity == 16);
}

静态变量不会被初始化,直到它们被使用了。

方法

静态方法和Java很像,是一个类层级的方法,不能使用this指针:

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

void main() {
  var a = Point(2, 2);
  var b = Point(4, 4);
  var distance = Point.distanceBetween(a, b);
  assert(2.8 < distance && distance < 2.9);
  print(distance);
}

注意:在创建广泛使用的api或者工具方法时考虑使用顶层方法而不是静态方法,它们是等效的,并且顶层方法更简洁。

你可以把静态方法当作一个编译时常量的函数对象来使用,比如你可以将静态方法作为参数传递给常量构造函数。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,729评论 2 9
  • 常量与变量使用let来声明常量,使用var来声明变量。声明的同时赋值的话,编译器会自动推断类型。值永远不会被隐式转...
    莫_名阅读 435评论 0 1
  • importUIKit classViewController:UITabBarController{ enumD...
    明哥_Young阅读 3,771评论 1 10
  • 标签(空格分隔): 未分类 基础(相关概念) 1.元祖 元组(tuples)把多个值组合成一个复合值。元组内的值可...
    一生信仰阅读 595评论 0 0
  • 听着某一首民谣想象自己可以毫无羁绊的去自己想去的远方,但我仍旧在学校完成我的学业,那是父母的期望。高考之后我似乎...
    何木梓阅读 125评论 0 0