Dart语言之关于类和对象所需要知道的一切

406 阅读7分钟

前言

Dart也是一种面向对象的语言,所以它也具有面向对象的基本概念,在类和对象层面同样具有封装,继承多态等。 当然它还有一些其他新的特性例如它的类可以当成接口使用,工厂构造方法,扩展方法等.

类的声名,成员变量,成员方法的声名和其它面向对象方式相同。

class Point {
    double? x; // 声明一个成员变量x, 并初始化为null.
    double? y; // 声明一个成员变量y, 并初始化为null.
    double z = 0; // 声明一个成员变量z, 并初始化为0.
}

这里我们在类型面前添加?表示它是可以为null的,没有?的成员说明它不能为null.实际这是Dart中的一种空类型,

为了防止在编程中出现空指针异常. 接下来我们来说说类的构造方式.

构造方法

在Dart中声明构造方法的方式有很多,常见的有以下几种

常规的构造函数

最普通的构造函数就是

class Point {
  double x = 0;
  double y = 0;

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

这种方式在Dart中实际上不经常会用到,更多的我们会用下面形式参数构造函数方式,更加简便.

形式参数构造函数

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

默认的构造函数

如果不去声明一个构造函数,那么这个类便拥有一个无参的默认构造函数, 同时如果该类有父类的话,默认构造函数还会调用父类的默认构造函数.

命名构造函数

我们还能给构造函数起别名,这样方便我们知道构造的意图。例如我们对Point创建一个命名构造函数,表明是原点对象.

const double xOrigin = 0;
const double yOrigin = 0;

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);

  // 命名构造函数
  Point.origin()
      : x = xOrigin,
        y = yOrigin;
}

调用父类的构造函数

在创建命名构造函数时候要调用父类的命名构造函数,需要单独声明,通常是后面跟super关键字加上对应父类的命名构造函数.

class Person {
  String? firstName;
  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  Employee.fromJson(super.data) : super.fromJson() {
    print('in Employee');
  }
}

void main() {
  var employee = Employee.fromJson({});
  print(employee);
}

另外在构造函数的参数里面还能加一个函数,它的执行在构造方法调用之前。

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

给父类参数赋值

通常子类会有一些参数需要赋值给父类,我们可以通过类似于之前提到的形式参数构造方法的方式给父类成员变量赋值.

class Vector2d {
  final double x;
  final double y;

  Vector2d(this.x, this.y);
}

class Vector3d extends Vector2d {
  final double z;
  // 传递x和y参数给父类的构造方法
  Vector3d(super.x, super.y, this.z);
}

这种方式相对以下更为简便了.

Vector3d(final double x, final double y, this.z) : super(x, y);

初始化列表

我们另外还有一种命名参数的花式用法,就是后面根一串初始化操作.

Point.fromJson(Map<String, double> json)
    : x = json['x']!,
      y = json['y']! {
  print('In Point.fromJson(): ($x, $y)');
}

这个实际上我个人觉得这种方式和写在方法体里面并没有什么区别,可能看上去更加整洁吧,这里它还能去叠加一些函数,例如判断参数是否满足要求

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

有些类成员属性需要一些逻辑计算后再赋值,下面distanceFromOrigin计算到原点的距离

import 'dart:math';

class Point {
  final double x;
  final double y;
  final double distanceFromOrigin;

  Point(double x, double y)
      : x = x,
        y = y,
        distanceFromOrigin = sqrt(x * x + y * y);
}

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

重定向构造函数

这种构造方式实际上是把构造过程委托给别的构造函数,它自己本身不做实现.

class Point {
  double x, y;

  // 主构造函数
  Point(this.x, this.y);

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

alongXAxis在这里实际上就是重定向构造函数

常量构造函数

如果你构造的对象始终不变,可以通过添加const关键字把它声明常量构造函数

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

  final double x, y;

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

工厂构造函数

这是所有构造方式中我觉得最有趣的方式,使用factory关键字去声明构造方法,可以不必每次都创建实例,使用

factory声明构造方法可以从缓存中返回实例,另外还有一个用处是逻辑化初始化final成员,这种操作可不能通

过List初始化方式完成。

举个例子: 下面的代码中,Logger factory构造从cache中返回对象,Logger.fromJson的factory构造初始化final成

员,这里初始化值来自于JSON对象属于逻辑化初始方式。

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

  static final Map<String, Logger> _cache = <String, Logger>{};

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

  factory Logger.fromJson(Map<String, Object> json) {
    return Logger(json['name'].toString());
  }

  Logger._internal(this.name);

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

调用方式上实际上和普通的构造方法没有区别,但是实际上走的factory构造方式的逻辑.

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

var logMap = {'name': 'UI'};
var loggerJson = Logger.fromJson(logMap);

成员

在类中我们可以声明一些成员,这里就包含了成员变量和成员方法.

成员变量

成员方法

成员方法的声明

成员方法可以访问类中的成员变量和其它成员方法,遇到参数和成员名字相同的情况也可以通过this表明是成员的情况。下面distanceTo就是一个成员方法.

import 'dart:math';

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);

  double distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
}

操作符重载

我们在Dart中可以重新定义操作符的执行逻辑,Dart允许我们去定义一下操作符.

image.png

我们通过使用operator关键字加上对应的操作符去定义操作的执行. 例如下面例子定义了Vertor的+,-, ==操作符的执行逻辑.

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

  @override
  bool operator ==(Object other) =>
      other is Vector && x == other.x && y == other.y;

  @override
  int get hashCode => Object.hash(x, y);
}

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

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

Getters和Setters

默认情况下,类中成员变量都具有一个get和set方法,我们可以重写这两种方法重新定义get和set的逻辑.

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

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

  // 定义了right和bottom成员的 get 和 set
  double get right => left + width;
  set right(double value) => left = value - width;
  double get bottom => top + height;
  set bottom(double value) => top = value - height;
}

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

抽象方法

如果需要把类中的一些方法留给子类去实现,可以将这些方法声明为抽象方法,同时该类也需要声明为抽象类不能够被实例化.

abstract class Doer {

  void doSomething(); // 定义一个抽象方法
}

class EffectiveDoer extends Doer {
  void doSomething() {
    //在子类中完成实现
  }
}

类继承

类的继承

使用 extends 关键字去继承一个类,使用 super 关键字去调用父类的方法.

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

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

方法重载

子类可以重写父类的方法,操作符,getters和setters, 可以通过添加@override注解表示你打算去重载一个方法.

class Television {
  // ···
  set contrast(int value) {...}
}

class SmartTelevision extends Television {
  @override
  set contrast(num value) {...}
  // ···
}

这里我们重载了contrast方法,对于方法重载,我们需要注意以下几个点

  • 返回的类型必须与重写方法类型相同
  • 每个参数的类型也必须相同,或者是它的超类,例如上面这个例子contrast的参数类型num是int的父类
  • 如果需要重载的方法声明有n个位置参数,那么你这里重载的方法也必须是n个位置参数.
  • 一个泛型方法不能重载为一个非泛型的方法,反之亦然.

noSuchMethod()

当调用一个类不存在的方法会抛出NoSuchMethodError异常,但是如果我们重写了noSuchMethod出现调用不存在的

方法就会调用该重写的方法,不会抛出异常了.

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

Mixins

通过使用Mixins我们可以重用一块代码(包括成员变量和方法),并且不会破坏原有的类层级结构

关于mixin的更多详情可以参考<关于Flutter中的mixins> juejin.cn/post/723259…

类的修饰符

通过使用类修饰符可以约束类的一些行为,Dart中提供了如下类的修饰符.

abstract

声明一个类为抽象类,只需要在类前面加上abstract关键字即可.抽象类不能够被实例化(使用factory方式除外),抽象类中的成员通常有抽象方法.

base

去限制该类只能继承不能被实现(接口),我们通过使用base修饰符, 加上base修饰符的类也可以被实例化,这点要和abstract区分开.

base class Vehicle {
  void moveForward(int meters) {
    // ...
  }
}
// 可以被实例化
Vehicle myVehicle = Vehicle();
base class Car extends Vehicle {
  int passengers = 4;
  // ...
}

// 不能被实现
base class MockVehicle implements Vehicle {
  @override
  void moveForward() {
    // ...
  }
}

final

加上final关键字类不能够被继承也不能够实现。这样做可以保证它不是一个对外扩展的类,可以安全的在类中做任何调整,不会影响到外界。同样在调用该类方法的时候也不用担心子类的实现会影响你的意图.

interface

通过添加interface关键字表示它是一个接口,可以让外部类实现它的方法。它可以被构造,可以被实现,但是不能够被继承.

interface class Vehicle {
  void moveForward(int meters) {
    // ...
  }
}
// 可以被构造
Vehicle myVehicle = Vehicle();

// 错误:不能够被继承
class Car extends Vehicle {
  int passengers = 4;
  // ...
}

// 可以被实现
class MockVehicle implements Vehicle {
  @override
  void moveForward(int meters) {
    // ...
  }
}

sealed

密封类用于创建一个已知的,枚举子类集合。它允许你在进行siwtch遍历的时候可以列举所有子类.

密封类不能够被实现和继承,密封类是隐式abstract的.

  • 它不能被构造
  • 它可以拥有factory构造器
  • 它可以定义构造器给子类进行使用

不过密封类的子类不是隐式abstract的

sealed class Vehicle {}

class Car extends Vehicle {}

class Truck implements Vehicle {}

class Bicycle extends Vehicle {}

// ERROR: 不能被实例化
Vehicle myVehicle = Vehicle();

// 子类可以被构造
Vehicle myCar = Car();

String getVehicleSound(Vehicle vehicle) {
  // ERROR: 这个switch语句缺少了Bicycle的情况,会报错.
  return switch (vehicle) {
    Car() => 'vroom',
    Truck() => 'VROOOOMM',
  };
}

mixin

mixin也是一个类的修饰符,关于mixin的作用之前已经提到过.

枚举

枚举声明

声明一个简单的枚举类型,只需要加上enum关键字即可, 然后罗列需要枚举的常量.

enum Color { red, green, blue }

增强枚举

实际上枚举可以声明的更复杂一些,我们称为增强枚举,包括它有成员变量,成员方法,构造函数等,但是这种声明方式有以下几点要求.

1, 成员变量必须是final类型

2,构造方法需要加上const关键字,表明是常量构造器

3, 不能够继承自其它类型(忘了说,枚举类默认的父类就是Enum)

4, index, hashCode, ==操作符,不能够被重写

5, 不能声明名字为values的成员,因为它是枚举默认的一个成员方法,返回所有的枚举值

6, 所有的枚举实例必须在开始时候声明,必须至少有一个枚举实例

enum Vehicle implements Comparable<Vehicle> {
  car(tires: 4, passengers: 5, carbonPerKilometer: 400),
  bus(tires: 6, passengers: 50, carbonPerKilometer: 800),
  bicycle(tires: 2, passengers: 1, carbonPerKilometer: 0);

  const Vehicle({
    required this.tires,
    required this.passengers,
    required this.carbonPerKilometer,
  });

  final int tires;
  final int passengers;
  final int carbonPerKilometer;

  int get carbonFootprint => (carbonPerKilometer / passengers).round();

  bool get isTwoWheeled => this == Vehicle.bicycle;

  @override
  int compareTo(Vehicle other) => carbonFootprint - other.carbonFootprint;
}

枚举使用

访问枚举值就和访问静态变量的方式一样

final favoriteColor = Color.blue;
if (favoriteColor == Color.blue) {
  print('Your favorite color is blue!');
}

index 属性

枚举成员有一个index属性,用于表明它在枚举声明中的位置(基于0开始)

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

name 属性

用于返回枚举值的名字,例如Color.blue会返回一个'blue'.

print(Color.blue.name); // 'blue'

values 属性

获取枚举的所有值以List形式.

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

switch case

使用枚举用来做switch操作有一个好处就是,如果没有列举完枚举的所有case, 会有编译警告.

var aColor = Color.blue;

switch (aColor) {
  case Color.red:
    print('Red as roses!');
    break;
  case Color.green:
    print('Green as grass!');
    break;
  default: // 没有这句,会有编译警告.
    print(aColor); // 'Color.blue'
}

扩展方法

声明扩展方法

命名扩展

命名扩展可以按照如下格式声明

extension <extension name>? on <type> {
  (<member definition>)*
}

Dart可以对原有类的方法进行扩展,例如下面我们String类进行扩展,添加一个String转换成Int的函数.

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }

  double parseDouble() {
    return double.parse(this);
  }
}
  • 扩展的不仅仅可以是方法,也可以对getters,setters还有操作符号进行扩展,这里不一一赘述.

不命名扩展

除了像上面一样为扩展方法的类起名外,也可以不命名进行扩展,不命名扩展仅能在当前文件中使用.

使用扩展方法

使用扩展方法和使用普通的成员方法是一样的,不过记得要导入声明的类库文件.

// 导入声明的类库文件
import 'string_apis.dart';
// ···
print('42'.padLeft(5)); // 使用String成员方法
print('42'.parseInt()); // 使用String的扩展方法

Static类型和dynamic

我们知道Dart中提供一种动态类型支持,我们可以声明一个string动态对象

dynamic d = '2';
print(d.parseInt());

但是这样会报错,因为扩展方法不支持动态类型. 因为扩展方法是针对静态类型的调用。

冲突

在扩展方法使用过程中可能会遇到方法名字冲突,例如我们分别在两个文件中对同一个类扩展一个同名方法,这个时候

使用的时候就会出现冲突。

规避冲突通常有以下几种手段.

使用hide和show关键字隐藏/展示扩展方法

// 定义扩展方法 parseInt().
import 'string_apis.dart';

// 这里也定义了 parseInt(), 但是可以通过hide进行隐藏.
import 'string_apis_2.dart' hide NumberParsing2;

// ···
//使用定义在'string_apis.dart'的扩展方法.
print('42'.parseInt());

显示的引用扩展

import 'string_apis.dart'; // 包含 NumberParsing 扩展.
import 'string_apis_2.dart'; // 包含 NumberParsing2 扩展.

// ···
// print('42'.parseInt()); // 不能用
print(NumberParsing('42').parseInt());
print(NumberParsing2('42').parseInt());

加入前缀

如果说上面例子string_apis_2.dart文件中的扩展也叫NumberParsing,那么我们可以对string_apis_2加入前缀. 同样也能去指定使用扩展方法.

// 以下两个libraries同时定义了parseInt()扩展函数。
import 'string_apis.dart';
import 'string_apis_3.dart' as rad;
// ···
// print('42'.parseInt()); // Doesn't work.
// 使用来自string_apis.dart.的扩展
print(NumberParsing('42').parseInt());
//  使用来自string_apis_3.dart.的扩展
print(rad.NumberParsing('42').parseInt());

Callable对象

Dart还提供了一个特性就是可以把对象当成方法用,我们只需要在类中实现call方法即可.

下面示范一个WannabeFunction去定义一个call方法,传递3个string参数然后连接它们,使用空格进行分割,最后增加一个感叹号.

class WannabeFunction {
  String call(String a, String b, String c) => '$a $b $c!';
}

var wf = WannabeFunction();
var out = wf('Hi', 'there,', 'gang');

void main() => print(out);