一文向你介绍Dart中类的使用与面向对象的思想

101 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第29天,点击查看活动详情

前言

今天我们介绍一下 Dart 中面向对象的知识。Dart 是一种面向对象的语言,具有类和基于 mixin 的继承。每个对象都是一个类的实例,所有的类都是 Object 的子类。基于 mixin 的继承意味着,尽管每个类(除了 Object)都只有一个超类,但类主体可以在多个类层次结构中重用。

类的基本使用

使用类成员

Dart 中的类的成员使用与 JavaScript 中的类似,在使用时只有些许的不同。

使用点号(.)引用实例变量或方法:

 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(可以看做是类似重载函数的构造函数)。

注: 在 dart 中 new 关键字为可选关键字。

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

有些类提供常量构造函数。要使用常量构造函数创建编译时常量,请将 const 关键字放在构造函数名之前:

 class Person{
     const Person();
 }
 var p = const Person();

构造两个相同的编译时常量会生成一个单一的、规范的实例(可以看做就是单例模式):

 var a = const ImmutablePoint(1, 1);
 var b = const ImmutablePoint(1, 1);
 
 assert(identical(a, b)); // They are the same instance!

当然,和前面讲 const 一样,在常量上下文中,可以在构造函数或文字之前省略 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关键字之外其他的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}');

实例变量

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

下面是如何声明实例变量的方法:

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

所有实例变量都生成隐式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 关键字是指当前实例。
  • 与 Java 中类似,this 在没有命名冲突的时候是可以省略的.

可以直接通过下面这种方法实现构造函数传递参数直接给对应的实例(与 TypeScript 中直接在形参上写上属性修辞符的用法有相似之处):

 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 = 1, this.y});
 }
 
 void main() {
   // 上面的this是语法同,本质还是x和y
   var p = new Point(y: 2);
   print(p.x);
   print(p.y);
 }

下面两点与 JAVA 中的构造函数类似:

  • 默认构造函数: 如果不声明构造函数(包括命名构造函数),则提供默认构造函数。默认构造函数没有参数,并在超类中调用无参数构造函数。

  • 构造函数不是继承: 对于子类和父类来说,子类不从父类继承构造函数。没有声明构造函数的子类只有默认的构造函数(没有参数,没有名称)而不是从父类继承的构造函数。

    • 子类必须调用父类的构造函数。默认情况下会自动调用父类的无参构造函数,如果父类没有无参构造函数,则必须显式的用super()调用一个构造函数。
    • 创建对象时,先调用父类的构造函数对对象进行初始化,然后再调用子类自己的构造函数。
    • 子类只默认调用父类的默认(无参)构造函数,如果父类重写了自己的构造函数,就会导致父类没有无参构造函数,这样子类就不能从父类继承构造函数。

命名的构造函数

使用命名构造函数可以在一个类中定义多个构造函数,或者让一个类的作用对于开发人员来说更清晰:

 class Point {
   num x, y;
 
   Point(this.x, this.y);
 
   // Named constructor
   Point.origin() {
     x = 0;
     y = 0;
   }
 }

注: 因为构造函数是不会从父类继承的,这意味着父类的命名构造函数子类也不会继承。如果希望使用在超类中定义的命名构造函数来创建子类,则必须在子类中实现该构造函数。

非默认的超类构造函数

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

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

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

注: dart 中的超类调用和其它的语言有所区别,不能在构造函数内部使用super()来调用,而是要在函数体之前就指定好要运行的超类函数。同时,子类的超类与父类的超类是可以不一致的(按照正常的代码风格,我们应该将其设置为同名的,这样才好管理),但是如果父类自定义了构造函数,子类必须要调用父类的其中一个构造函数。

 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';
 }
 
 /// 结果输出为
 /*
 in Person
 in Employee
 */

因为父类构造函数的参数是在调用构造函数之前执行的,所以参数可以是表达式,比如函数调用:

 class Employee extends Person {
   Employee() : super.fromJson(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关键字(左边是可以的,如果没有命名冲突也可以省略 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);
 }
 
 /// 运行结果
 /*
 3.605551275463989
 */

注意: 当初始化列表和超类的构造函数一起使用时,超类的构造函数要写在最后面,前面也是用逗号隔开。

 class Animal {
   int x;
   int y;
   Animal(this.x, this.y);
 }
 
 class Dog extends Animal {
   int z;
   Dog(int x, int y, int z)
       : this.z = z,
         super(x, y);
 }
 
 void main() {
   var dog = new Dog(1, 2, 3);
   print(dog.x); // 1
   print(dog.y); // 2
   print(dog.z); // 3
 }

重定向构造函数

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

 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 的。

这样的构造函数实例出的对象属性是不能通过x.xx = xx来改变的,是一个编译时就创建好的常量。同时传入相同值得实例都是同一个实例

 class ImmutablePoint {
   static final ImmutablePoint origin =
       const ImmutablePoint(0, 0);
 
   final num x, y;
 
   const ImmutablePoint(this.x, this.y);
 }
 void main() {
   var a = const ImmutablePoint(1, 1);
   var b = const ImmutablePoint(1, 1);
 
   print(a == b); // true
 }

注意: 常量构造函数不能够有函数体,也就是后面的{}

工厂构造函数

在实现构造函数时使用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);
   }
 }

调用工厂构造函数,就像调用其他构造函数一样:

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

注意:

  • 工厂构造函数不能访问 this 关键字,所以使用变量默认会使用类的静态属性。

  • 工厂函数中缓存的实例都会是同一个,类似常量构造函数使用单例模式。

     var logger = Logger('UI');
     var logger1 = Logger('UI');
     print(logger == logger1); // true
    

方法

实例方法

对象上的实例方法可以访问实例变量。下面示例中的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);
   }
 }

getter 和 setter 方法

getter 和 setter 是对对象属性的读写访问的特殊方法。从前面我们可以知道,每个实例变量都有一个隐式的 getter,如果需要的话还可以加上一个 setter。使用getset关键字来实现 getter 和 setter 方法可以来读写其他属性:

 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;
   void set right(num value) => left = value - width;
   num get bottom => top + height;
   set bottom(num value) => top = value - height;
   
   // 非箭头函数写法,getter函数后面没有(),不能传入参数
   num get area {
     return this.height * this.width;
   }
   void set areaHeight(num value) {
     this.height = value;
   }
 }
 ​
 test() => {
       if (true) {print("success")}
     };
 void main() {
   var rect = Rectangle(3, 4, 20, 15);
   assert(rect.left == 3);
   rect.right = 12;
   assert(rect.left == -8);
 }

注意:

  • getter 与 setter 函数在使用时就如同实例的普通属性的赋值与读取一样使用,而不是像函数一样使用。

  • getter 函数与其他函数有所不同,因为不能传入参数,所有 dart 在语法中都没有()给我们使用。

  • setter 的函数类型只能不写或者是 void 类型,同时虽然是 void 类型但是和普通的变量赋值一样默认还是有赋值的变量作为返回值的。

     // 如果是其他返回值是void的函数使用函数返回值是会报错的
     print(rect.left = 3); // 3
    
  • 诸如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...
   }
 }

注意: dart 中实现抽象方法直接在类名前面加上 abstract 关键字使类变成抽象类就可以了,不需要在特定的方法前面加上 abstract 关键字,没有方法体的方法 dart 会自动将其看做抽象方法

继承

与 Java 和 JavaScript 中一致,使用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关键字。

重写操作符

可以重写下表中显示的操作符。例如,如果定义一个Vector类,可以定义一个+方法来让两个向量相加。

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

下例在类中重写了+-操作符:

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

不能调用未实现的方法,除非下列任何一个是正确的:

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

类(静态)变量和方法

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

可以使用静态方法作为编译时常量。例如,可以将静态方法作为参数传递给常量构造函数。

私有成员

在 dart 中的类定义私有成员不像 Java 那样有公开、保护和私有的关键字。如果标识符以下划线(_)开头,则该标识符对其库是私有的。

注意: 私有的类成员在同一个库中是无效的,必须要在不同的库中被引入才能够作为私有成员。在同一个库中使用会被当做是一个普通的成员变量。

 import 'B.dart';
 
 void main() {
   var b = new B();
   b.run();
   b._run(); // Error
 }

隐式接口

dart 中有接口的概念,但是没有专门单独定义接口的关键字interface,在 dart 中的接口使用类来进行代替(一般使用抽象类),每个类都隐式地定义一个接口,该接口包含类的所有实例成员及其实现的任何接口。

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

可调用的类

实现call()方法可以让 dart 类的实例像函数一样被调用,相当于实例化返回了一个函数对象。

在下面的示例中,WannabeFunction类定义了一个call()函数,该函数接受三个字符串并将它们连接起来,每个字符串用空格分隔,并在结尾加一个感叹号。

 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');
 }
 
 /// 执行结果
 /*
 Hi there, gang!
 */

mixins

mixins 是在多个类层次结构中重用类代码的一种方式。mixins 的中文意思是混入,就是在类中混入其他功能,在 dart 中可以使用 mixins 实现类似多继承的功能。

要使用 mixin,需要在 with 关键字后面加上一个或多个 mixin 名称。下面的例子显示了两个使用 mixin 的类:

 class Musician extends Performer with Musical {
   // ···
 }
 
 // 当继承和mixins用在一起是extends关键字应该在with之前
 class Maestro extends Person
     with Musical, Aggressive, Demented {
   Maestro(String maestroName) {
     name = maestroName;
     canConduct = true;
   }
 }

因为 mixins 使用的条件,随着 dart 版本一直在变,这里说明一下 dart 2.x 中的使用条件:

  • 作为 mixins 的类只能继承 Object 类(也就是创建的时候不能使用 extends 关键字),不能继承其他类。
  • 作为 mixins 的类不能有构造函数。
  • 一个类可以 mixins 多个 mixins 类。
  • mixins 绝不是继承,也不是接口,而是一种全新的特性。
 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');
     }
   }
 }

mixins 使用时注意点:

  • 当继承的类和mixins的类有属性或方法名冲突时,后面应用的类的属性或方法会顶替掉前面应用的。

  • mixins 后的类实例的成员是运用到的所有类的实例。

     class Maestro extends Person
         with Musical, Aggressive, Demented {
       Maestro(String maestroName) {
         name = maestroName;
         canConduct = true;
       }
     }
     void main(){
         var m = new Maestro();
         print(m is Person); // true
         print(m is Musical); // true
         print(m is Aggressive); // true
         print(m is Demented); // true
     }