dart入门潜修面向对象篇之类和构造方法

本文收录于dart入门潜修系列教程

创作不易,转载还请备注。

前言

dart是一门面向对象的语言,一定程度上和java很像,但同时也有自己的实现机制。

提起面向对象,自然而然会想到面向对象的三大特征:继承、封装、多态。dart显然也满足这三类特征,但是却与java、c++中的实现有所不同。众所周知,java中的继承是单继承,而c++允许多继承,这两种方式显然各有利弊。但单继承显然看似会更合理些。

为什么这么说呢?这让我想起了印象很深刻的、关于继承的一个举例:如果有一个动物,继承了会奔跑的马的特性,同时又继承了会飞的老鹰的特性,那么这个动物会是什么样的?这显然只是个说笑的例子,但是也从侧面反映了单继承的合理性以及多继承可能会引起的复杂性。所以java采用了单继承机制。

dart同java一样,类之间同样是单继承的,但是,dart提供了Mixin机制,用于实现“多继承”功能。

继承并不是本篇文章的主题,这里只是作为个引子,引出我们今天的主题 — 类及其构造方法。

这个大家都很熟悉,是对事物属性、行为的一种抽象。dart中同样有类的概念,dart同java一样,所有类都继承于一个基类Object,而且所有的对象都是某个类的实例,来看个例子:

class Person {
  String name; //实例变量
  Person(String name) {    //构造方法
    this.name = name;//注意this关键字
  }
}
//main方法
void main() {
  Person p = new Person("张三");//生成一个Person类实例
  print(p.name);
  p = Person("李四");//dart2 支持省略new关键字,这里新产生了一个Person类实例
  print(p.name);
  p.name = "王五";//我们也可以通过实例变量完成赋值
  print(p.name);
}

看到上面代码发现,和java何其相似!确实,dart中定义类的语法和java很像, dart定义类的关键字是class,而且在定义类的时候,如果没有提供构造方法,同样会由系统提供一个默认的无参构造方法。当我们提供了有参构造方法的时候,无参构造方法将会自动失效,除非我们又显示定义了其无参构造方法。

我们可以通过new 类名()的方式来生成一个类对象实例,当然根据dart的语法,我们也可以省略new关键字(从dart2开始)。

上面代码中,我们同时定义了一个实例变量name,然后通过对象访问符点(.) 完成对其的访问。dart中的实例变量可以在定义的时候就完成初始化,比如上面代码中,我们可以为name赋值一个默认值:

  String name = "无名"; //实例变量

实例变量如果没有初始化则默认为null,如果有初始化值,则该值会在实例构造的时候完成赋值,换句话说,实例变量的初始化会发生在构造方法之前,示例如下:

class Person {
  String name = "无名"; //实例变量
  Person(String name) {
    print(this.name);//这里会打印'无名'
    this.name = name;
  }
}
//main方法
void main() {
  Person p = new Person("张三");//生成一个Person对象,会执行Person的构造方法
  print(p.name);
}

上面打印结果为:

无名
张三

由此验证了,实例变量初始化会发生在构造方法之前的论断。

最后,dart中同样提供了一个我们非常熟悉的关键字:this,表示当前对象,所以我们可以在构造方法中通过this来完成对其属性成员的赋值。结合this,可以简写构造方法,如下所示:

class Person {
  String name; //实例变量
  Person(this.name) {//注意这里,入参直接是this.name,这将会在构造方法body执行之前,完成对实例变量name的赋值
  }
}
//测试main方法
void main() {
  Person p = Person("张三");
  print(p.name);//打印'张三'
}

我们都知道,在kotlin中(如有兴趣,可参考kotlin入门潜修),提供了 ?.操作符以避免空指针问题,在dart中,同样也提供了该操作符,示例如下:

void main() {
  Person p = null;//p为null
  print(p.name);//!!!错误,这里会crash,因为p为null
  print(p?.name);//正确,打印null
}

上面代码,在单纯使用点(.)操作符访问对象的实例成员的时候,如果对象为null则会抛出异常,而使用?.操作符访问的时候,则会打印null,这是因为?.操作符只有在对象不为null的时候才会继续执行(即访问该对象中的成员),否则打印null。

构造方法

在上一小节中,我们已经简单使用过了dart中的构造方法,也知道了dart会提供默认无参构造方法,本小节,将继续对构造方法进行探索。

命名构造方法

顾名思义,可以为构造方法指定一个有意义的名字,示例如下:

class Person {
  String name; //实例变量
  Person(String name) {//主构造方法
    this.name = name;
  }
//这个就是命名构造方法,注意其语法
  Person.name(String name){
    this.name = name;
  }
}
//main方法
void main() {
  Person person = Person.name("张三");//采用命名构造方法构造person对象
  print(person.name);//打印'张三'
}

构造方法的继承性

在dart中,子类默认会调用父类的无参非命名构造方法(即主构造方法),如下所示:

class Person {
  Person() {
    print("in person constructor");
  }
}
//子类Student,继承Person
class Student extends Person {
  Student(){
    print("in student constructor");
  }
}

void main() {
  Student student = new Student();
}

上面的代码写法是正确的,执行完后,打印如下所示:

in person constructor
in student constructor

这说明,在dart中,子类会默认调用父类的无参非命名构造方法,其执行顺序会首先执行父类中的构造方法,然后再执行子类自身的构造方法。

那么如果有多个构造方法怎么办?不急,先来看个例子:

class Person {
  Person(){
  }
  Person(String name) {
    print("in person constructor");
  }
}

上面这段代码是java中极其常见的代码:即提供了不同参数的构造方法。然而,这在dart中是错误的!即dart不允许同时定义多个以类名为命名的构造方法,在dart中这种构造方法被称为主构造方法,主构造方法有且只能定义一个,如果想提供多个构造方法,则需要提供命名构造方法,如下所示:

class Person {
  Person() {//主构造方法
  }
//提供了一个接受入参的命名构造方法
  Person.name(String name) {
    print("in person constructor");
  }
}

那么,这个时候子类的构造方法该如何来写?来看个例子:

class Person {
  Person() {
    print("in person constructor");
  }
  Person.name() {
    print("in person name constructor");
  }
}
class Student extends Person {
  Student() {//默认依然继承父类的无参主构造方法
    print("in student constructor");
  }
}
//测试方法
void main() {
  Student student = Student();
}

上面代码执行后,打印如下所示:

in person constructor
in student constructor

从打印结果可知,子类初始化并没有调用父类命名构造方法,而是调用的默认主构造方法。那么如何调用到命名的构造方法呢?

这里有两种方法,一种是通过super关键字,如下所示:


class Person {

  Person() {
    print("in person constructor");
  }

  Person.name() {
    print("in person name constructor");
  }
}

class Student extends Person {
  Student() :super.name(){//注意这里,我们通过super完成了对命名构造方法的调用
    print("in student constructor");
  }
}

另一个则是通过间接构造方法,由父类完成调用,如下所示:

class Person {
  Person() :this.name();//委托给了name构造方法
  Person.name(){
    print("in person name constructor");
  }
}

class Student extends Person {
  Student() {//子类默认调用的是父类中的无参主构造方法
    print("in student constructor");
  }
}

void main() {
  Student student = Student();
}

上面代码执行完后,打印如下:

in person name constructor
in student constructor

即我们通过间接委托机制,完成了对其他构造方法的调用。

常量构造方法

什么是常量构造方法?常量构造方法就是使用const修饰的构造方法,那么常量构造方法有什么意义?先来看段代码:

class Person {
  final String name; //name必须声明为final的
  const Person(this.name); //定义了一个常量构造方法,注意不允许有body
}

void main() {
  Person p1 = const Person("张三"); //只有构造方法被const修饰的时候,才可以在对象之前加const
  Person p2 = const Person("张三");
  print(identical(p1, p2));//打印true
  Person p3 = const Person("张三");
  Person p4 = Person("张三");
  print(identical(p3, p4));//打印false
  Person p5 = const Person("张三");
  Person p6 = const Person("李四");
  print(identical(p5, p6));//打印false
}

由上面代码可知,构造方法使用const修饰后,可以在一定条件下生成唯一的实例,这里之所以说唯一,是因为在有些条件下依然会生成多个实例,具体参考上面的代码场景。

初始化列表

dart中提供了一种机制,可以在构造方法执行之前完成变量初始化,这种机制是通过初始化列表来完成的,如下所示:

class Person {
  String name; 
  int defAge;
  String defSex;
  Person(this.name)
      :defAge = 1,//这就是初始化列表
        defSex = "男" {
    print("person constructor...");
  }
}

void main() {
  Person person = Person("张三");
  print("age = ${person.defAge}; name = ${person.defSex}");
}

上面代码执行完后,打印如下:

person constructor...
age = 1; name = 男

由此可知,初始化列表的执行顺序要先于构造方法执行。其语法是写在构造方法之后,使用冒号(:)分割,然后列表中的多个变量使用逗号分割(,),注意,最后一个变量无需使用逗号。其伪语法为:构造方法:初始化列表{}。

工厂构造方法

使用工厂构造方法,我们可以缓存相关实例,代码如下所示:

class Person {
  String name; 
  static final Map<String, Person> _cache = <String, Person>{};//定义了缓存容器
  Person.create(this.name);//命名构造方法
//工厂构造方法,使用关键字factory修饰
  factory Person(String name) {
    if (_cache.containsKey(name)) {//如果有缓存则直接返回
      print("cache");
      return _cache[name];
    } else {//否则,生成一个新对象
      print("not cache");
      final person = Person.create("张三");
      _cache[name] = person;
      return person;
    }
  }
}
//测试main方法
void main() {
  Person person = Person("张三");
  Person person2 = Person("张三");
}

代码执行完成后,打印如下:

not cache
cache

工厂方法使用factory关键字修饰,从打印结果可知,第二次我们获取“张三”实例的时候,已经是取的缓存中数据了。

但是,仔细看上面的代码,你会发现,这完全可以通过静态方法来实现,如我们可以通过提供一个静态方法getPerson,来完成同factory 构造方法一样的功能:

static Person getPerson(String name){
    if (_cache.containsKey(name)) {
      print("cache");
      return _cache[name];
    } else {
      print("not cache");
      final person = Person.create("张三");
      _cache[name] = person;
      return person;
    }
  }

确实如此,那么二者有什么区别呢?显然factory构造方法,允许你像使用普通构造方法一样构造对象,而不是像通过方法调用来生成实例。

获取对象类型

众所周知,java中可以通过反射拿到当前对象的具体类型,以此来完成各种动态操作。在dart中,同样提供了获取对象类型的方法,而且很简单,如下所示:

void main() {
  Person p = Person("张三");
  print(p.runtimeType);//打印Person
}

至此,本篇文章阐述完毕。