面向对象

概述

所谓面向对象,是相对于面向过程而言的。那什么是面向过程呢?C语言就是一种典型的面向过程的编程语言。其实过程也就是所谓的步骤。有一个经典例子是这样的,如何把大象放进冰箱?

  1. 把冰箱门打开
  2. 把大象放进去
  3. 把冰箱门关上

有些人可能会觉得荒诞,大象怎么能放得进冰箱呢?然而这就是面向过程的思维方式,C语言代码如下

void openDoor(){}
void put(void *){}
void closeDoor(){}

int main(){
    // 打开门
    openDoor();
    // 放进去
    put(obj);
    // 关上门
    closeDoor();
}

每一个步骤对应到代码其实就是一个函数,每一个函数实现一个功能,然后分步调用这些函数。这些函数可能是我们自己写的,也可能是别人写的,函数实际上是一个黑盒模型,这个盒子是封闭的,我们不知道里面有什么,只知道这个盒子有一个入口和一个出口,就如同ATM机,我们把卡插到入口,另一边出口就冒钱出来了,至于具体的,钱是怎么冒出来的,这不是我们关心的。回到上面的例子,大象能不能放进冰箱,这也不是我们关心的,总之这个函数就是把大象放进冰箱的函数,只管调用它就好了。

有了这种面向过程的思维方式,编程就变得简单清晰有条理了,我们可以先把整个架子先搭起来,所有的函数先空实现,整个架子建好了,再慢慢去实现这一个个函数的具体细节。这跟建房子一样,先把钢结构架子搭起来,然后再慢慢码砖砌墙,最后才是室内装修。

随着软件业的发展,需求越来越复杂,人们发现面向过程的思维模型太简单了,已经无法胜任日益复杂的软件需求了,于是就出现了面向对象的思维方式。面向对象既是一种思维模型,也是一种代码的组织形式。

面向对象核心的载体是类和对象。那什么是类?什么是对象呢?

什么是对象

要说清楚这个问题,得先解释什么是对象,不然还怎么去面向对象呢。在面向对象的哲学里,有一句话是“一切皆对象!”对象一词实际上是从英语翻译过来的,这个翻译其实是不准确的,最重要的就是没有指明这个概念的内涵。它的英文object实际上表达的是具体事物,客观事物,客体的意思。其实就是将具体事物抽象化,用一句星爷电影《功夫》中的台词来解释就是“那个谁”的意思,就是将一切的具体事物,抽象出一个共同的指代模型,你也可以说“那个东西”、“那个事物”,你在说这句话时,一定是指的一个具体存在的东西,而不是一个空泛的虚无的东西,这就是对象的特点。

什么是类

了解了对象,我们不禁要问,编程中怎么创建对象,怎么运用对象呢?可以试想一下,假设我们现在想要描述猫这种动物,该怎么做?

首先可以观察具体的猫,然后将所有猫都具备的特征提取出来,抽象出来,这个抽象出来的模型也就是。例如,猫都有尾巴,有毛,圆眼竖瞳,喜欢睡觉,昼伏夜出,会抓老鼠,会喵喵叫,喜欢吃鱼等等。这里我们就提取几个特征,形成一个猫类

  • 圆眼竖瞳
  • 有皮毛
  • 吃鱼
  • 抓老鼠
  • 睡觉

有了类,我们就可以判断一只猫是否属于猫类,也可以根据这个类批量创造猫。可以看出,类其实就是一个设计蓝图,或者说是一个模具,所有依据这个蓝图创造的具体的猫都是这个类的一个对象。类就是一个图纸,对象就是这个图纸的具体事物。

类所包含的特征,我们通常分为两种类型,属性和行为。属性是静态的描述,行为是动态的特征。以上面的猫类为例

属性 行为
圆眼竖瞳 吃鱼
有皮毛 抓老鼠
睡觉

行为往往是以动词开头,在编程中用使用函数来表示,而属性则使用变量来表示。纯粹的面向对象编程语言是Java和C#,其次支持面向对象的还有C++和Python等。

类的定义

// Dart中定义一个类
class Person {
  String name;
  int age;

  Person(String name, int age) {
    this.name = name;
    this.age = age;
  }
}

Dart中的类与Java中的相似,不同的是,Dart中没有privatepublic这些成员访问修饰符。如果是类私有的成员,不希望外面访问,只需要在成员变量之前加上一个下划线_变为私有即可。

以上代码,在Dart中还有一种简化写法,可以自动在构造方法中对成员变量初始化。

// Dart中定义一个类
class  Person {
    String name;
    int age;

    // 在构造方法中初始化成员变量时,可使用如下写法简化
    Person(this.name, this.age);

    // 如需处理其他变量时,也可单独对其操作
    // Person(this.name, this.age, String address){
    //     print(address);
    // }
    // 注意,构造方法不能重载,以上注释掉
}

另外还需要注意一点,Dart中没有构造方法的重载,不能写两个同名的构造方法。

Getters 和 Setters方法

在Java中,一般不会直接在类的外部去访问类成员,通常使用setter和getter方法来操作类的成员变量。而在Dart语言中,所有类中都包含隐式的getter方法,对于非final修饰的成员,类中还包含隐式的setter方法。这就意味着,在Dart中,你可以直接在类外部通过.操作符访问类成员。这一特点使得Dart语法更加简洁,不会写出满屏的setXXX、getXXX方法。

当然,很多时候我们调用setter和getter方法并不仅仅是为了赋值和访问,而是为了一些额外的处理,这时候我们只需要使用setget关键字实现setter和getter方法即可。

class  Person {
    String userName;

    Person(this.userName);

    // 方法名前加get关键字
    String get name{
        return  "user:"  +  this.userName;
    }

    // 方法名前加set关键字
    set name(String name){
        // do something
        this.userName = name;
    }
}

void  main() {
    var p = new Person("zhangsan");
    print(p.name);   // user:zhangsan
    p.name = "Jack";
    print(p.name);   // user:Jack
}

要注意,在创建对象时,new关键字并不是必须的,可以省略不写。在写Flutter界面时,不建议写new关键字实例化对象,因为Flutter框架中没有类似的xml语言来描述UI界面,界面也是使用Dart语言来写,在使用Dart写UI时,要保持代码的简洁和结构化,省略new会更友好。

构造方法

如果没有定义构造方法,则会有一个默认的无参构造方法,并且会调用超类的无参构造方法。

命名构造方法

上面已经说过,Dart类中两个同名构造方法不能重载,但是Dart语言为类新增了一种称为命名构造方法的东西。

class  Person {
    String userName;
    int age;

    Person(this.userName, this.age);

    // 命名构造方法
    Person.fromData(Map data) {
        this.userName = data['name'];
        this.age = data['age'];
    }
}

void  main() {
    // 使用命名构造方法创建对象
    var p = new Person.fromData({
        "name":"Bob",
        "age":19
    });
    print(p.userName);
}

注意,使用命名构造方法可以为一个类实现多个构造方法,也可以更清晰的表明意图。

常量构造方法

如果想提供一个状态永远不变的对像,在Dart中,我们可以创建一个编译时常量对象,节省开销。

class  ConstPoint {
    final num x;
    final num y;

    // 使用const修构造方法
    const ConstPoint(this.x, this.y);

    // 编译时常量对象,需使用const来创建对象
    static final ConstPoint origin = const  ConstPoint(0, 0);
}

void  main() {
    print(ConstPoint.origin.x);
    print(ConstPoint.origin.y);
}

工厂构造方法

当我们需要创建一个新的对象或者从缓存中取一个对象时,工厂构造方法就派上了用场。

class  Logger {
    final String name;

    // 创建一个静态Map做为缓存
    static final Map<String, Logger> _cache =  <String, Logger>{};

    // 定义一个命名构造方法,用下划线"_"修饰,将构造方法私有化
    Logger._internal(this.name);

    // 使用关键字factory修饰类同名构造方法
    factory Logger(String name) {
        if (_cache.containsKey(name)) {
            return _cache[name];
        } else {
            // 调用命名构造方法创建新对象
            final logger= new  Logger._internal(name);
            _cache[name] = logger; // 存入缓存
            return logger;
        }
    }
}

void  main() {
    var uiLog = new Logger('UI');
    var eventLog = new Logger('event');
}

构造方法重定向

有时候一个构造方法会调动类中的其他构造方法来实例化,这时候可以使用构造方法重定向,

class Point {
  num x;
  num y;

  // 同名构造方法
  Point(this.x, this.y);

  // 命名构造方法重定向到同名构造方法,中间使用一个冒号
  Point.alongXAxis(num x) : this(x, 0);
}

类的初始化列表

熟悉C++的朋友应该对初始化列表很了解了,Java中是没有这个特性的。

class  Point {
    final  num x;
    final  num y;
    final  num distance;

    Point(x, y)
        : x = x,
          y = y,
          distance =  sqrt(x * x + y * y){
             print("这是构造方法");
          }
}

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

在这里插入图片描述

  • 初始化列表位于构造方法的小括号与大括号之间,在初始化列表之前需添加一个冒号。
  • 初始化列表是由逗号分隔的一些赋值语句组成。
  • 它适合用来初始化 final修饰的变量
  • 初始化列表的调用是在构造方法之前,也就是在类完成实例化之前,因此初始化列表中是不能访问 this

运算符重载

这个特性,又很类似于C++中的运算符重载,在Java中是没用这种概念的。

class Point {
  int x;
  int y;

  Point(this.x, this.y);

  // 使用operator关键字,为该类重载"+"运算符
  Point operator +(Point p) {
    return new Point(this.x + p.x, this.y + p.y);
  }

  // 为该类重载"-"运算符
  Point operator -(Point p) {
    return new Point(this.x - p.x, this.y - p.y);
  }
}

void main(){
   var p1 = new Point(1,5);
   var p2 = new Point(7,10);

   // 重载运算符后,类可以使用“+”、“-” 运算符操作
   var p3 = p1 + p2;
   var p4 = p2 - p1;

   print("${p3.x}, ${p3.y}");
   print("${p4.x}, ${p4.y}");
}

打印结果:

8, 15
6, 5

Dart中允许重载的运算符如下:

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

类的继承与混入

Dart中的继承,与Java中相似,可以使用关键字extends继承父类,使用关键字super引用父类

class Father {
    myFunction(){
        // do something
    }
}

class Son extends Father {

    @override
    myFunction(){
        super.myFunction();
        // do something
    }
}

我们知道,Java中的类仅支持单继承,而Dart中的类也只支持单继承。但是Dart可以使用一种被称为混入的方式来达到多继承的效果,这需要使用with关键字。

// 首先定义三个父类
class Father1 {
    a(){
      print("this is a func");
    }

    common(){
        print("common Father1");
    }
}

class Father2 {
    b(){
      print("this is b func");
    }

    common(){
        print("common Father2");
    }
}

class Father3 {
    c(){
      print("this is c func");
    }

    common(){
        print("common Father3");
    }
}

//定义子类
class Son extends Father1 with Father2,Father3{

}

void main() {
  var obj = new Son();
  obj.common();
  obj.a();
  obj.b();
  obj.c();
}

打印结果:

common Father3
this is a func
this is b func
this is c func

要注意,以上写法中,也可以直接使用with,等价于如下写法

class Son with Father1,Father2,Father3{

}

混入(mixin)

有时候我们需要一种在多个类层次结构中重用代码的方法,混入正是为了解决这个问题而出现的。

  • mixin可以实现类似多重继承的功能,但是多重继承中相同函数并不会存在父子关系
  • mixin 实现了一条线性继承链,声明的顺序代表了继承的顺序,如果存在同名方法,那么声明在最后的混入中的方法会覆盖前面的
  • mixin是其超类的子类型,也是mixin名称本身表示的类的子类型
  • 最新的SDK版本,在mixin中可以调用super

mixin的另一种使用方式


mixin Walker{
    void walk(){
        print("walk");
    }
}

mixin Swim{
    void swim(){
        print("swim");
    }
}

// 声明抽象方法
mixin Flying {
  void flying();
}

mixinon关键字的配合

// 限定使用范围,要混入Swim必须是Human的子类
mixin Swim on Human {

}

接口抽象

抽象类

Dart语言没有提供interface关键字来定义接口,但是Dart语言中保留了抽象类,同Java,使用abstract关键字来修饰抽象类。而Dart中的抽象类,实际上就相当于Java中的接口。

abstract class Base {
    // 省略函数体即可定义抽象方法,不需加关键字
    func1();
    func2();
}

注意,抽象类是不能被实例化的,子类继承抽象类时,必须实现全部抽象方法。

隐式接口

实际上在Dart中,每个类都隐式的定义了一个包含所有实例成员的接口, 并且该类实现了这个接口。

因此,如果我们想实现某个接口,但有又不想继承,则可以使用这种隐式接口机制。我们需要用到关键字implements

class People {
  void greet(){
    print("Hello");
  }
}

class Student implements People{
  @override
  void greet(){
    print("Hi,I'm Alice.");
  }
}

greet(People p){
  p.greet();
}

void main() {
  greet(new Student());
}

公众号“编程之路从0到1”

20190301102949549

Copyright © Arcticfox 2020 all right reserved,powered by Gitbook文档修订于: 2024-06-09 20:54:25

results matching ""

    No results matching ""