Dart语法篇之面向对象基础(五)

简述:缓存

从这篇文章开始,咱们继续Dart语法篇的第五讲, dart中的面向对象基础。咱们知道在Dart中一切都是对象,因此面向对象在Dart开发中是很是重要的。此外它还和其余有点不同的地方,好比多继承mixin、构造器不能被重载、setter和getter的访问器函数等。数据结构

1、属性访问器(accessor)函数setter和getter

在Dart类的属性中有一种为了方便访问它的值特殊函数,那就是setter,getter属性访问器函数。实际上,在dart中每一个实例属性始终有与之对应的setter,getter函数(如果final修饰只读属性只有getter函数, 而可变属性则有setter,getter两种函数)。而在给实例属性赋值或获取值时,实际上内部都是对setter和getter函数的调用ide

一、属性访问器函数setter

setter函数名前面添加前缀set, 并只接收一个参数。setter调用语法于传统的变量赋值是同样的。若是一个实例属性是可变的,那么一个setter属性访问器函数就会为它自动定义,全部实例属性的赋值实际上都是对setter函数的调用。这一点和Kotlin中的setter,getter很是类似。函数

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

  Rectangle(this.left, this.top, this.width, this.height);
  set right(num value) => left = value - width;//使用set做为前缀,只接收一个参数value
  set bottom(num value) => top = value - height;//使用set做为前缀,只接收一个参数value
}

main() {
  var rect = Rectangle(3, 4, 20, 15);
  rect.right = 15;//调用setter函数时,能够直接使用相似属性赋值方式调用right函数。
  rect.bottom = 12;//调用setter函数时,能够直接使用相似属性赋值方式调用bottom函数。
}
复制代码

对比Kotlin中的实现源码分析

class Rectangle(var left: Int, var top: Int, var width: Int, var height: Int) {
    var right: Int = 0//在kotlin中表示可变使用var,只读使用val
        set(value) {//kotlin中定义setter
            field = value
            left = value - width
        }
    var bottom: Int = 0
        set(value) {//kotlin中定义setter
            field = value
            top = value - height
        }
}

fun main(args: Array<String>) {
    val rect = Rectangle(3, 4, 20, 15);
    rect.right = 15//调用setter函数时,能够直接使用相似属性赋值方式调用right函数。
    rect.bottom = 12//调用setter函数时,能够直接使用相似属性赋值方式调用bottom函数。
}
复制代码

二、属性访问器函数getter

Dart中全部实例属性的访问都是经过调用getter函数来实现的。每一个实例数额行始终都有一个与之关联的getter,由Dart编译器提供的。post

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

  Rectangle(this.left, this.top, this.width, this.height);
  get square => width * height; ;//使用get做为前缀,getter来计算面积.
  set right(num value) => left = value - width;//使用set做为前缀,只接收一个参数value
  set bottom(num value) => top = value - height;//使用set做为前缀,只接收一个参数value
}

main() {
  var rect = Rectangle(3, 4, 20, 15);
  rect.right = 15;//调用setter函数时,能够直接使用相似属性赋值方式调用right函数。
  rect.bottom = 12;//调用setter函数时,能够直接使用相似属性赋值方式调用bottom函数。
  print('the rect square is ${rect.square}');//调用getter函数时,能够直接使用相似读取属性值方式调用square函数。
}
复制代码

对比kotlin实现ui

class Rectangle(var left: Int, var top: Int, var width: Int, var height: Int) {
    var right: Int = 0
        set(value) {
            field = value
            left = value - width
        }
    var bottom: Int = 0
        set(value) {
            field = value
            top = value - height
        }

    val square: Int//由于只涉及到了只读,因此使用val
        get() = width * height//kotlin中定义getter
}

fun main(args: Array<String>) {
    val rect = Rectangle(3, 4, 20, 15);
    rect.right = 15
    rect.bottom = 12
    println(rect.square)//调用getter函数时,能够直接使用相似读取属性值方式调用square函数。
}
复制代码

三、属性访问器函数使用场景

其实,上面settergetter函数实现的目的,普通函数也能作到的。可是若是用setter,getter函数形式更符合编码规范。既然普通函数也能作到,那具体何时使用setter,getter函数,何时使用普通函数呢。这不得不把这个问题和另外一问题转化一下成为: 哪一种场景该定义属性仍是定义函数的问题(关于这个问题,记得好久以前在讨论Kotlin的语法详细介绍过)。咱们都知道函数通常描述动做行为,而属性则是描述状态数据结构(状态可能通过多个属性值计算获得)。 若是类中须要向外暴露类中某个状态那么更适合使用setter,getter函数;若是是触发类中的某个行为操做,那么普通函数更适合一点。this

好比下面这个例子,draw绘制矩形动做更适合使用普通函数来实现,square获取矩形的面积更适合使用getter函数来实现,能够仔细体会下。编码

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

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

  set right(num value) => left = value - width; //使用set做为前缀,只接收一个参数value
  set bottom(num value) => top = value - height; //使用set做为前缀,只接收一个参数value
  get square => width * height; //getter函数计算面积,描述Rectangle状态特性
  bool draw() {
    print('draw rect'); //draw绘制函数,触发是动做行为
    return true;
  }
}

main() {
  var rect = Rectangle(3, 4, 20, 15);
  rect.right = 15; //调用setter函数时,能够直接使用相似属性赋值方式调用right函数。
  rect.bottom = 12; //调用setter函数时,能够直接使用相似属性赋值方式调用bottom函数。
  print('the rect square is ${rect.square}');
  rect.draw();
}
复制代码

2、面向对象中的变量

一、实例变量

实例变量实际上就是类的成员变量或者称为成员属性,当声明一个实例变量时,它会确保每个对象实例都有本身惟一属性的拷贝。若是要表示实例私有属性的话就直接在属性名前面加下划线 _,例如_width_heightspa

class Rectangle {
  num left, top, _width, _height;//声明了left,top,_width,_height四个成员属性,未初始化时,它们的默认值都是null
}  
复制代码

上述例子中的left, top, width, height都是会自动引入一个gettersetter.事实上,在dart中属性都不是直接访问的,全部对字段属性的引用都是对属性访问器函数的调用, 只有访问器函数才能直接访问它的状态。

二、类变量(static变量)与顶层变量

类变量实际上就是static修饰的变量,属于类的做用域范畴;顶层变量就是定义的变量不在某个具体类体内,而是处于整个代码文件中,至关于文件顶层,和顶层函数差很少意思。static变量更多人愿意把它称为静态变量,可是在Dart中静态变量不只仅包括static变量还包括顶层变量

其实对于类变量和顶层变量的访问都仍是经过调用它的访问器函数来实现的,可是类变量和顶层变量有点特殊,它们是延迟初始化的,在getter函数第一次被调用时类变量或顶层变量才执行初始化,也便是第一次引用类变量或顶层变量的时候。若是类变量或顶层变量没有被初始化默认值仍是null.

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {
    Cat() {
        print("I'm a Cat!");
    }
}
//注意,这里变量不定义在任何具体类体内,因此这个animal是一个顶层变量。
//虽然看似建立了Cat对象,可是因为顶层变量延迟初始化的缘由,这里根本就没有建立Cat对象
Animal animal = Cat();
main() {
    animal = Dog();//而后又将animal引用指向了一个新的Dog对象,
}
复制代码

顶层变量是具备延迟初始化过程,因此Cat对象并无建立,由于整个代码执行中并无去访问animal,因此没法触发第一次getter函数,也就致使Cat对象没有建立,直接表现是根本就不会输出I'm a Cat!这句话。这就是为何顶层变量是延迟初始化的缘由,static变量同理。

三、final 变量

在Dart中使用 final 关键字修饰变量,表示该变量初始化后不能再被修改。final 变量只有 getter 访问器函数,没有 setter 函数。相似于Kotlin中的val修饰的变量。声明成final的变量必须在实例方法运行前进行初始化,因此初始化final变量有不少中方法。注意: 建议尽可能使用final来声明变量

class Person {
    final String gender = '男';//直接在声明的时候初始化,这种方式比较局限,针对基本数据类型还能够,但若是是一个对象类型就显示不合适了。
    final String name;
    final int age;
    Person(this.name, this.age);//利用构造函数为final变量初始化。
    //上述代码等价于下面实现
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
复制代码

finalconst的区别,就比如Kotlin中的valconst val之间的区别,const是编译期就进行了初始化,而 final则是运行期进行初始化

四、常量对象

在dart有些对象是在编译期就能够计算的常量,因此在dart中支持常量对象的定义,常量对象的建立须要使用 const 关键字。常量对象的建立也是调用类的构造函数,可是注意必须是常量构造函数,该构造函数是用 const 关键字修饰的。常量构造函数必须是数字、布尔值或字符串,此外常量构造函数不能有函数体,可是它能够有初始化列表。

class Point {
    final double x, y;
    const Point(this.x, this.y);//常量构造函数,使用const关键字且没有函数体
}

main() {
    const defaultPoint = const Point(0, 0);//建立常量对象
}
复制代码

2、构造函数

一、主构造函数

主构造函数是Dart中建立对象最普通一种构造函数,并且主构造函数只能有一个,若是没有指定主构造函数,那么会默认自动分配一个默认无参的主构造函数。此外dart中构造函数不支持重载

class Person {
    var name;
    //隐藏了默认的无参构造函数Person();
}
//等价于:
class Person {
    var name;
    Person();//通常把与类名相同的函数称为主构造函数
}
//等价于
class Person {
    var name;
    Person(){}
}

class Person {
    final String name;
    final int age;
    Person(this.name, this.age);//显式声明有参主构造函数
    Person();//编译异常,注意: dart不支持同时重载多个构造函数。
}
复制代码

构造函数初始化列表 :

class Point3D extends Point {
    double z;
    Point3D(a, b, c): z = c / 2, super(a, b);//初始化列表,多个初始化步骤用逗号分隔;先初始化z ,而后执行super(a, b)调用父类的构造函数
}
//等价于
class Point3D extends Point {
    double z;
    Point3D(a, b, c): z = c / 2;//若是初始化列表没有调用父类构造函数,
    //那么就会存在一个隐含的父类构造函数super调用将会默认添加到初始化列表的尾部
}
复制代码

初始化的顺序以下图:

初始化实例变量几种方式

//方式一: 经过实例变量声明时直接赋默认值初始化
class Point {
    double x = 0, y = 0;
}

//方式二: 使用构造函数初始化方式
class Point {
    double x, y;
    Point(this.x, this.y);
}

//方式三: 使用初始化列表初始化
class Point {
    double x, y;
    Point(double a, double b): x = a, y = b;//:后跟初始化列表
}

//方式四: 在构造函数中初始化,注意这个和方式二仍是有点不同的。
class Point {
    double x, y;
    Point(double a, double b) {
        x = a;
        y = b;
    }
}
复制代码

二、命名构造函数

经过上面主构造函数咱们知道在Dart中的构造函数是不支持重载的,实际上Dart中连基本的普通函数都不支持函数重载。 那么问题来了,咱们常常会遇到构造函数重载的场景,有时候须要指定不一样的构造函数形参来建立不一样的对象。因此为了解决不一样参数来建立对象问题,虽然抛弃了函数重载,可是引入命名构造函数的概念。它能够指定任意参数列表来构建对象,只不过的是须要给构造函数指定特定的名字而已。

class Person {
  final String name;
  int age;

  Person(this.name, this.age);

  Person.withName(this.name);//经过类名.函数名形式来定义命名构造函数withName。只须要name参数就能建立对象,
  //若是没有命名构造函数,在其余语言中,咱们通常使用函数重载的方式实现。
}

main () {
  var person = Person('mikyou', 18);//经过主构造函数建立对象
  var personWithName = Person.withName('mikyou');//经过命名构造函数建立对象
}
复制代码

三、重定向构造函数

有时候须要将构造函数重定向到同一个类中的另外一个构造函数重定向构造函数的主体为空,构造函数的调用出如今冒号(:)以后

class Point {
    double x, y;
    Point(this.x, this.y);
    Point.withX(double x): this(x, 0);//注意这里使用this重定向到Point(double x, double y)主构造函数中。
}
//或者
import 'dart:math';

class Point {
  double distance;

  Point.withDistance(this.distance);

  Point(double x, double y) : this.withDistance(sqrt(x * x + y * y));//注意:这里是主构造函数重定向到命名构造函数withDistance中。
}
复制代码

四、factory工厂构造函数

通常来讲,构造函数老是会建立一个新的实例对象。可是有时候会遇到并非每次都须要建立新的实例,可能须要使用缓存,若是仅仅使用上面普通构造函数是很难作到的。那么这时候就须要factory工厂构造函数它使用factory关键字来修饰构造函数,而且能够从缓存中的返回已经建立实例或者返回一个新的实例。在dart中任意构造函数均可以被替换成工厂方法, 它看起来和普通构造函数没什么区别,可能没有初始化列表或初始化参数,可是它必须有一个返回对象的函数体

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) {//使用factory关键字声明工厂构造函数,
    if (_cache.containsKey(name)) {
      return _cache[name]//返回缓存已经建立实例
    } else {
      final logger = Logger._internal(name);//缓存中找不到对应的name logger,调用_internal命名构造函数建立一个新的Logger实例
      _cache[name] = logger;//并把这个实例加入缓存中
      return logger;//注意: 最后返回这个新建立的实例
    }
  }

  Logger._internal(this.name);//定义一个命名私有的构造函数_internal

  void log(String msg) {//实例方法
    if (!mute) print(msg);
  }
}
复制代码

3、抽象方法、抽象类和接口

抽象方法就是声明一个方法而不提供它的具体实现。任何实例的方法均可以是抽象的,包括getter,setter,操做符或者普通方法。含有抽象方法的类自己就是一个抽象类,抽象类的声明使用关键字 abstract.

abstract class Person {//abstract声明抽象类
  String name();//抽象普通方法
  get age;//抽象getter
}

class Student extends Person {//使用extends继承
  @override
  String name() {
    // TODO: implement name
    return null;
  }

  @override
  // TODO: implement age
  get age => null;
}
复制代码

在Dart中并无像其余语言同样有个 interface的关键字修饰。由于Dart中每一个类都默认隐含地定义了一个接口

abstract class Speaking {//虽然定义的是抽象类,可是隐含地定义接口Speaking
  String speak();
}

abstract class Writing {//虽然定义的是抽象类,可是隐含地定义接口Writing
  String write();
}

class Student implements Speaking, Writing {//使用implements关键字实现接口
  @override
  String speak() {//重写speak方法
    // TODO: implement speak
    return null;
  }

  @override
  String write() {//重写write方法
    // TODO: implement write
    return null;
  }
}
复制代码

4、类函数

类函数顾名思义就是类的函数,它不属于任何一个实例,因此它也就不能被继承。类函数使用 static 关键字修饰,调用时能够直接使用类名.函数名的方式调用。

class Point {
    double x,y;
    Point(this.x, this.y);
    static double distance(Point p1, Point p2) {//使用static关键字,定义类函数。
        var dx = p1.x - p2.x;
        var dy = p1.y - p2.y;
        return sqrt(dx * dx + dy * dy);
    }
}
main() {
    var point1 = Point(2, 3);
    var point2 = Point(3, 4);
    print('the distance is ${Point.distance(point1, point2)}');//使用Point.distance => 类名.函数名方式调用
}
复制代码

总结

到这里有关dart中面向对象基础部分已经介绍完毕,这篇文章主要介绍了dart中经常使用的构造函数以及一些面向对象基础知识,下一篇咱们将继续dart中面向对象一些高级的东西,好比面向对象的继承和mixin. 欢迎关注~~~

个人公众号

这里有最新的Dart、Flutter、Kotlin相关文章以及优秀国外文章翻译,欢迎关注~~~

Dart系列文章,欢迎查看:

相关文章
相关标签/搜索