header-bg.jpg
Dart 面向对象之类
发表于 2019-07-10 12:38
|
分类于 Dart
|
评论次数 0
|
阅读次数 395

面向对象的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。

Dart 也是面向对象的语言,同时支持 mixin 的继承机制。Dart 的每个对象都是一个类的实例,所有的类都继承于 Object 类,基于 mixin 的继承意味着每个类( Object 类除外)只有一个超类(父类),一个类的代码可以在其他多个子类中使用,有过编程经验的人一般都知道这个概念,所以我就不再赘述。

创建类

和大多数语言一样,使用 class 关键字作为前缀创建一个类,使用 extends 继承另外一个类,类中可以包含变量和函数:

class Bar {
  const String name = 'bar';

  test() => print('test success');
}

class Foo extends Bar {

}

理解对象

使用 new 关键字可以创建一个对象:

void main() {
  final foo = new Foo();
  print(foo);// output: Instance of 'Foo'
}

Dart 2 以后的版本可以省略构造函数之前的关键字 new,也就是说下面这种写法和上面的写法是等价的:

void main() {
  final foo = Foo();
  print(foo);// output: Instance of 'Foo'
}

使用 . 操作符来访问对象的方法或属性:

void main() {
  final foo = Foo();

  print(foo);// output: Instance of 'Foo'
  print(foo.name);// output: bar
  foo.test();// output: test success
}

上面的代码中,如果变量 foo 的值为 null,那么使用 foo.name 访问属性将会报错,而使用 ?. 操作符就可以避免这个问题:

void main() {
  var foo;
  print(foo?.name);// output: null
}

扩展类

子类使用 extends 关键字继承超类,使用 super 关键字引用超类的变量或函数,并且子类可以覆写超类的实例函数、Getter 和 Setter,例如:

class Person {
  final String sex = 'boy';
  final int age = 18;
  getInfo() => 'name: $sex, age: $age';
}

class Employee extends Person {
  final String name = 'leo';
  final String position = 'php';
  
  getInfo() {
    final info = super.getInfo();
    print('$info, name: $name, position: $position');
  }
}


main() => Employee().getInfo();// name: boy, age: 18, name: leo, position: php

上面的代码中, Person 类的 getInfo 方法被 Employee 类的 getInfo 方法覆盖了。

我们还可以使用 @override 注解来表明某函数是想覆写超类的同名函数,如果覆写错误,编辑器将会提示错误,用法如下:

class A {
  
  noSuchMethod(Invocation mirror) => print('You tried to use a non-existent member:' + '${mirror.memberName}');
}

void main() {
  dynamic a = A();
  a.test();// output: You tried to use a non-existent member:Symbol("test")
}

上面的代码中 ,A 类覆写了 Object 类的 noSuchMethod 函数,当调用 A 类中不存在的函数时,会被 A 类中的 noSuchMethod 函数捕获到。

函数

函数是类中定义的方法,是类对象的行为。

构造函数

Dart 的构造函数和 PHP 5 一样,在类中声明一个与类名相同的函数就可以创建一个构造函数。

如果不主动声明构造函数,系统将提供一个默认的构造函数,默认的构造函数是没有名字、没有参数的,并且在被实例化时,系统将自动调用父类的无参数构造函数(若参数都是可选参数,也会被自动调用)。

下面的例子在 Person 类中主动声明了一个构造函数,并初始化 sexage 属性的值:

class Person {
  String sex;
  int age;

  Person({sex = 'girl', age = 18}) {
    this.sex = sex;
    this.age = age;
  }
}

上面的代码中 this 指向 Person 实例,使用 this 关键字可以访问 Person 类中的属性和方法。

注意: 只有当名字冲突的时候才使用 this。否则的话, Dart 代码风格样式推荐忽略 this。

由于将构造函数的参数赋值给属性这种写法非常频繁,所以 Dart 提供了语法糖,让我们可以更简洁地实现上面的功能:

class Person {
  String sex;
  int age;

  Person({this.sex = 'girl', this.age = 18});
}

命名构造函数

通过命名构造函数可以让一个类拥有多个构造函数,提供更有针对性的构造函数。

命名构造函数的命名格式为 ClassName.identifier,用法如下:

class Person {
  String sex;
  int age;

  Person({this.sex = 'girl', this.age = 18});

  Person.baby({this.sex = 'girl', this.age = 3});
}

上面的代码中,Person 类提供了 baby 命名构造函数,我们使用 Person.baby() 即可创建一个新的实例:

main() {
  final baby = Person.baby();

  print(baby.sex);// output: girl
  print(baby.age);// output: 3
}

调用超类构造函数

子类不会继承超类的构造函数,默认情况下,子类的构造函数会在执行之前自动调用超类的无参数构造函数(若超类的构造函数都是可选参数,也会被子类自动调用)。

超类若定义了必须传递参数的构造函数,子类的构造函数则不会自动调用超类的构造函数,并且子类必须手动调用超类的构造函数。

例如下面的代码中, Person 类声明了 2 个构造函数,第一个构造函数规定必须传递 2 个参数:

class Person {
  final sex;
  final age;

  Person(this.sex, this.age);

  Person.baby({this.sex = 'girl', this.age = 3});
}

接着,再定义一个 Employee 类,继承 Person 类,此时 Employee 类就必须手动调用 Person 构造函数。使用 : 操作符和 super 关键字即可进行手动调用:

class Employee extends Person {
  final name;
  final salary;
  
  Employee({this.name = 'leo', this.salary = 3000}) : super('girl', 18);
}

main() {
  final people = Employee();

  print(people.name);// output: leo
  print(people.salary);// output: 3000
  print(people.sex);// output: girl
  print(people.age);// output: 18
}

如果想调用 Person 类的 baby 命名构造函数,使用 super.baby() 即可调用:

class Employee extends Person {
  final name;
  final salary;

  Employee({this.name = 'leo', this.salary = 3000}) : super.baby();
}

main() {
  final people = Employee();

  print(people.name);// output: leo
  print(people.salary);// output: 3000
  print(people.sex);// output: girl
  print(people.age);// output: 3
}

initialize list(初始化列表)

构造函数后的 : 后面,除了可以使用 super 关键字调用超类的构造函数,还可以初始化实例参数。

多个表达式使用 , 分隔即可,这些表达式被称之为初始化列表,下面是使用示例:

class Person {
  final sex;
  final age;

  Person(this.sex, this.age);

  Person.baby({this.sex = 'girl', this.age = 3});
}

class Employee extends Person {
  final name;
  final salary;
  final position;

  Employee(Map people)
      : name = people['name'],
        salary = people['salary'],
        position = people['position'],
        super.baby();
}

main() {
  final people = Employee({
    'name': 'leo',
    'salary': 3000,
    'position': 'php'
  });

  print(people.name);// output: leo
  print(people.salary);// output: 3000
  print(people.position);// output: php
  print(people.sex);// output: girl
  print(people.age);// output: 3
}

如果在初始化参数列表中使用 super 关键字调用父类的构造函数,推荐将它放到列表的最后,详情参考 Dart 最佳实践

构造函数执行顺序

如果提供了初始化列表,则初始化列表会在超类构造函数执行之前被调用,下面是构造函数的执行顺序:

(1)initialize list(初始化列表)

(2)super class’s no-arg constructor(超类的无参数构造函数)

(3)main class’s no-arg constructor(主类的无参数构造函数)

重定向构造函数

顾名思义,即调用该构造函数会重定向到其他构造函数。重定向函数是没有代码的,使用 :this 调用其他构造函数:

class Person {
  final sex;
  final age;

  Person(this.sex, this.age);

  Person.baby(sex) : this(sex, 1);
}

void main() {
  final leo = Person.baby('girl');
  print(leo.sex);// output: girl
  print(leo.age);// output: 1
}

常量构造函数

当一个类的所有变量都使用 final 声明时,我们可以为这个类定义常量构造函数:

class Person {
  final sex;
  final age;

  const Person(this.sex, this.age);
}

上面的代码中,Person 函数就是一个常量构造函数,常量构造函数在被实例化时可以使用 const 声明。
使用 const 声明的对象如下特点:

当有多个变量都实例化了 Person 对象时,如果传入的参数一样时,内存中就只会引用同一个实例,例如:

void main() {
  const leo = Person('boy', 18);
  const leo2 = Person('boy', 18);
  print(leo == leo2);// output: true;
}

下面这种写法是和上面等价的:

void main() {
  final leo = const Person('boy', 18);
  var leo2 = const Person('boy', 18);
  print(leo == leo2);// output: true;
}

如果你不使用 const 关键字,那么实例化后的对象将是两个不同的对象:

void main() {
  final leo = Person('boy', 18);
  final leo2 = Person('boy', 18);
  print(leo == leo2);// output: false;
}
注意:只有包含常量构造函数的类在被实例化时才能使用 const 关键字

工厂构造函数

当我们实例化构造函数时每次都会创建一个新的对象,如果我们希望并不是每次都创建新对象,则可以使用 factory 关键字来声明一个工厂构造函数。

工厂构造函数与普通构造函数不同,工厂构造函数不会自动生成实例,而是通过代码来决定返回的实例对象,例如:

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

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

  factory Logger(String name) {
    if (_cache.containsKey(name)) return _cache[name];

    final logger = Logger._internal(name);
    _cache[name] = logger;
    return logger;
  }

  Logger._internal(this.name);

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

上面的代码中,Logger 类拥有一个工厂构造函数,该函数每次都会先判断私有变量 _cache 中是否存在相同键名的实例,如果存在就会直接返回该实例,不存在就会生成一个新的实例,并将实例缓存至 _cache 中,后面的访问都会从缓存中取出同键名的实例。

调用 Logger 类:

void main() {
  final logger = Logger('UI');
  final logger2 = Logger('UI');
  logger.log('Button clicked');// output: Button clicked
  logger2.log('Button clicked');// output: Button clicked
  print(logger == logger2);// output: true
}

从输出结果可以看出,Logger('UI') 虽然被调用了 2 次,但是它们其实返回的是同一个实例。

工厂构造函数类似于 static 静态成员,无法访问 this 指针,所以在工厂构造函数中能处理的任务较为有限,一般都会定义一个命名构造函数用于生产实例,例如上面例子中的 _internal 函数就是提供给工厂函数的,用于生产 Logger 实例。

实例函数

对象的实例函数可以访问 this 指针,例如:

class Person {
  final String name;
  final String sex;
  final int age;

  Person({this.sex = 'boy', this.age = 18, this.name = 'leo'});

  String info() {
    return "${this.name}'s age is ${this.age}, sex is ${this.sex}";
  }
}

void main() {
  final employee = Person();
  print(employee.info());// output: leo's age is 18, sex is boy
}

静态变量与静态函数

在类中的变量或函数名前使用 static 关键字作为前缀,即可声明静态变量和静态函数,调用格式为 ClassName.identifier

静态变量在第一次使用时才会被初始化,下面是使用静态变量的示例:

class Color {
  static const green = const Color('#008000');
  final String code;
  const Color(this.code);
}

main() => print(Color.green.code);// output: #008000

静态函数也是在第一次使用时才会被初始化,它不在类的实例上执行,所以无法访问 this,并且调用静态函数并不会执行构造函数,例如:

class Person {

  Person() {
    print("Person Class is init");
  }

  static String info() {
    return "leo's height is 180cm";
  }
}

main() => print(Person.info());// output: leo's height is 180cm

Getter 和 Setter 函数

和其他面向对象的语言一样,Dart 也具有重载功能,就是利用 Getter 和 Setter 来实现。

Getter 和 Setter 是设置和访问对象属性的特殊函数,每个实例变量在底层都具有一个 Getter,如果变量不是 finalconst 所声明的,则还具有一个 Setter。

使用 getset 关键字即可为不可访问的属性定义 Getter 和 Setter:

class Person {
  String name;
  String familyName;

  Person(this.name, this.familyName);

  String get fullName => '${this.name} ${this.familyName}';
  set fullName(String value) {
    final nameMap = value.split(' ');
    this.name = nameMap[0];
    this.familyName = nameMap[1];
  }
}

void main() {
  final singer = Person('B.B.', 'King');
  print(singer.fullName);// output: B.B. King
  singer.fullName = 'Taylor Swift';
  print(singer.name);// output: Taylor
  print(singer.familyName);// output: Swift
}

上面的代码中,在读取 singer 实例的 fullName 属性时,会调用 get,在设置 fullName 属性时,会自动调用 set

覆写操作符的函数

Dart 中类的操作符是可以被覆写的,使用 operator 关键字与指定的操作符,即可为该操作符定义执行的逻辑,例如:

class Vector {
  final int x;
  final int y;

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

void main() {
  final foo = Vector(1, 2);
  final bar = Vector(3, 4);
  final foz = foo + bar;
  final baz = foo - bar;

  print(foz.x);// output: 4
  print(foz.y);// output: 6
  print(baz.x);// output: -2
  print(baz.y);// output: -2
}

上面的代码中,Vector 类中定义了 + 操作符和 - 操作符的执行函数,在对 Vector 的实例 foo 进行加减运算时,实例 bar 会作为形参被传入相应的函数中,最后会返回一个新的 Vector 实例。

call 函数

call 函数是一个很有趣的东西,它可以让我们以调用方法的形式去调用一个类,相关用法如下:

class Person {
  call(String firstName, String lastName) => print('Hi, $firstName $lastName');
}

void main() {
  final singer = Person();
  singer('B.B.', 'King');// output: Hi, B.B. King
}

可以看出,call 函数其实就是一个别名功能,下面的代码和上面是等价的:

void main() {
  final singer = Person();
  singer.call('B.B.', 'King');// output: Hi, B.B. King
}

访问控制(可见性)

Dart 类中所有变量和函数默认是 public 对外公开的,也就是说在任何地方都可以访问,我们可以通过在变量名或函数名的前面添加 _ 前缀将其私有化,被私有化的变量和函数只能被其定义所在的库中访问(每个 Dart app 都是一个库)。

Mixin

Mixin 是一种代码复用机制,它可以使一个类继承多个类,功能类似于 PHP 的 trait

使用 with 关键字即可使用此功能,例如:

class Person {
  String sex = 'man';
}

class Musical {
  bool canPlayPiano = false;
}

class Maestro extends Person with Musical {
  Maestro() {
    print(sex);
    print(canPlayPiano); 
  }
}

main() => Maestro();// output: man false

上面的代码中,Maestro 类继承了 Person 类,还复用了 Musical 类,如果你需要复用更多的类,使用 , 隔开即可:

class Maestro extends Person with Musical, Aggressive {

}

枚举类

枚举类型通常称之为 Enumerations 或 Enums,它是一种特殊的类,用来表现一个固定数目的常量,它不能被继承或实现、不能使用 mixin 以及不能被实例化。

使用 enum 关键字可以定义枚举类,例如:

enum Color {
  red,
  green,
  blue
}

枚举类中的每个值都有一个 index 的 Getter 函数,该函数返回当前值在枚举类中的位置,和数组的索引类似(即下标从 0 开始自增),例如:

void main() {
  print(Color.red.index);// output: 0
  print(Color.green.index);// output: 1
  print(Color.blue.index);// output: 2
}

每个枚举类都有一个 values 常量,访问此常量会返回一个包含当前枚举类全部值的 List,例如:

main() => print(Color.values);// output: [Color.red, Color.green, Color.blue]

我们可以使用 switch 语句来判断枚举类的值,需要注意的是我们必须在 case 语句中处理枚举类的全部值,否则编辑器会提示错误,例如:

void main() {
  final blue = Color.blue;
  switch(blue) {
    case Color.blue:
      print('is blue');
      break;
    case Color.green:
      print('is green');
      break;
    /*default:
      print('is red');
      break;*/
  }
}

抽象类

使用 abstract 修饰符可以定义抽象类,和其他面向对象的语言一样,抽象类是不能被实例化的,只能使用 extends 关键字进行继承。

抽象类中一般包含抽象函数,抽象函数即没有内容的空函数,由子类来实现抽象函数,下面是一个具有抽象函数的抽象类的示例:

abstract class Book {
  void updateArticle();
}

上面的代码中,Book 类是不能被实例化的,需要由子类来实现 updateArticle 方法:

class Article extends Book {
  void updateArticle() {
    print('updated');
  }
}

main() => Article().updateArticle();// output: updated

隐式接口

Dart 中的每个类都隐式地定义了包含所有实例成员的接口,并且每个类都实现了自己的接口。

如果我们想使用 A 类来支持 B 类的接口,而又不想继承 B 类的实现,则可以对 A 类使用 implements 关键字来实现 B 类即可。

class Person {
  final String _name;

  Person(this._name);

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

class Imposter implements Person {
  final String _name = '';
  
  greet(who) => print('Hi $who. Do you know who I am?');
}

main() => Imposter().greet('leo');// output: Hi leo. Do you know who I am?

上面的代码中,Imposter 类只实现了 Person 类的接口,如果需要实现更多的接口,则使用 , 连接,例如:

class Imposter implements Person, Employee, Student {
  // ...
}

发布评论
还没有评论,快来抢沙发吧!