Dart 2 (七) 类

Dart是一种面向对象的语言,带有类和基于mixin的继承。每个对象都是类的实例,所有类都是从对象继承而来。基于mixin的继承意味着,尽管每个类(对象除外)都只有一个超类,但类主体可以在多个类层次结构中重用。

使用类成员

对象具有由函数和数据(分别是方法和实例变量)组成的成员。当您调用一个方法时,您在一个对象上调用它:该方法可以访问该对象的函数和数据。
使用点(.)指向实例变量或方法:

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;

使用构造函数

您可以使用构造函数创建对象。构造函数名可以是ClassName或ClassName.identifier。例如,下面的代码使用Point()和Point. fromjson()构造函数创建对象:

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

版本提示:在Dart 2中,new 关键字变成了可选的。
有些类提供常量构造函数。要使用常量构造函数创建编译时常量,请将const关键字放在构造函数名之前:

var p = const ImmutablePoint(2, 2);

构造两个相同的编译时常量会导致一个单独的实例:

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

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

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

const pointAndLine = const {
  'point': const [const ImmutablePoint(0, 0)],
  'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};

版本提示:在Dart 2中,const关键字在一个恒定的上下文中成为可选的。

获取对象的类型

要在运行时获得对象的类型,可以使用对象的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.
}

所有未初始化的实例变量的值都为null。
所有实例变量都会生成隐式getter方法。非最终实例变量也生成隐式setter方法。

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.
}

构造函数

通过创建与类同名的函数来声明构造函数(另外,还可以选择添加一个在命名构造函数中描述的附加标识符)。最常见的构造函数形式是生成构造函数,它创建一个类的新实例:

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关键字引用当前实例。
注意:只有在名称冲突时才使用“this”。否则Dart会忽略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;
  }
}

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

调用非默认超类构造函数

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

  1. 初始化器列表
  2. 父类的无参数构造函数
  3. main类的无参数构造函数

如果超类没有未命名的、无参数的构造函数,那么您必须手动调用超类中的一个构造函数。在构造函数主体(如果有的话)之前的冒号(:)后面指定父类构造函数。

初始化器列表

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

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

重定向构造函数

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

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

方法

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

实例方法

对象上的实例方法可以访问实例变量等等。下面示例中的distanceTo()方法是一个实例方法的示例:

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。您可以通过实现getter和setter来创建额外的属性,使用get和set关键字:

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,您可以从实例变量开始,然后用方法包装它们,而无需更改客户机代码。
注意:诸如increment(++)这样的操作符以预期的方式工作,不管getter是否显式定义。为了避免任何意外的副作用,操作符只调用一次getter,将它的值保存在一个临时变量中。

抽象方法

实例、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...
  }
}

使用 abstract 修饰符定义无法实例化的抽象类a类。抽象类对于定义接口非常有用,通常带有一些实现。如果您希望您的抽象类看起来是可实例化的,请定义一个工厂构造函数。
抽象类通常有抽象方法。下面是一个声明具有抽象方法的抽象类的例子:

abstract class AbstractContainer {
  // Define constructors, fields, methods...

  void updateChildren(); // Abstract method.
}

隐式接口

每个类隐式地定义一个接口,其中包含类的所有实例成员及其实现的任何接口。如果您想要创建一个A类 支持类B 的API ,而不继承B,那么类a应该实现B接口。

// A person. The implicit interface contains greet().
class Person {
  // In the interface, but visible only in this library.
  final _name;

  // Not in the interface, since this is a constructor.
  Person(this._name);

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

// An implementation of the Person interface.
class Impostor implements Person {
  get _name => '';

  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 {...}

扩展一个类

使用extend来创建子类,使用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”关键字。

重写运算符

可以覆盖下表中显示的操作符。例如,如果您定义了一个向量类,您可以定义一个+方法来添加两个向量。

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

下面是一个类的例子,它覆盖了+和-运算符:

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

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

你不能调用一个未实现的方法,除非下列之一是true:

  • 接收者拥有静态类型dynamic
  • 接收方有一个定义未实现方法的静态类型(抽象是可以的),而接收方的动态类型有一个noSuchMethod()的实现,与类对象中的实现不同。

枚举类型

枚举类型,通常称为枚举或枚举,是一种特殊的类,用于表示固定数量的常量值。

使用枚举

使用enum关键字声明枚举类型:

enum Color { red, green, blue }

枚举中的每个值都有一个索引getter,它返回枚举声明中值的从零开始的位置。例如,第一个值的索引值为0,第二个值的索引值为1。

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

要获得枚举中的所有值的列表,请使用枚举值常量。

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

您可以在switch语句中使用枚举,如果不处理enum的所有值,就会收到警告:

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

枚举类型有以下限制:

  • 您不能子类化、混入或实现枚举。
  • 您不能显式地实例化枚举。

向类添加特性:mixins

mixins是在多个类层次结构中重用类代码的一种方法。
要使用mixin,请使用后跟一个或多个mixin名称的with关键字。下面的例子展示了两个使用mixin的类:

  // ···
}

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

要实现mixin,创建一个扩展对象的类,声明没有构造函数,也没有对super的调用。例如:

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

类变量和方法

使用static关键字实现类范围的变量和方法。
静态变量
静态变量(类变量)对于类范围的状态和常量非常有用:

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

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

静态变量在使用之前不会初始化。
静态方法
静态方法(类方法)不操作实例,因此不能访问它。例如:

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

注意:考虑使用通用或广泛使用的实用程序和功能的顶级函数,而不是静态方法。
可以使用静态方法作为编译时常量。例如,您可以将静态方法作为参数传递给常量构造函数。

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

推荐阅读更多精彩内容