
4.1 类
和其他面向对象的高级编程语言类似,Dart所有的类都是Object的子类,即继承于Object类。不同的是,Dart的基本数据类型属于对象,甚至null也属于对象。因此,可以说Dart是一种真正面向对象的语言。
4.1.1 类的实例化
首先来看下面一段代码:


运行结果:

在上面的代码中,有一段代码和main()方法的缩进一样,即Person类。这个类是一个自定义的类,其大括号包含了这个类的实现。实际上,在使用String类的实例时,String作为基本数据类型,其类不需要自定义,直接拿来用即可,所以看不到String类的源码。在使用Person类时,可以把它当作String类看待。
在定义类时,我们可以像上面的例子一样直接把多个类写到一个文件中,也可以用单独的一个文件来实现一个类。例如,对于上例而言,新建一个文件,命名为Person.dart,把Person类的具体内容写到这个文件中,然后删掉原先的Person类实现。由于删掉了Person类的实现内容,原先的代码会报错。此时,需要指明要使用的类,即import(导入)Person类。最终的代码将如下所示:
.dart代码:

Person.dart代码:


运行结果和前面的相同。通常意义上讲,为了确保代码的可读性和可维护性,会将不同的类放在不同的文件中单独处理。一个完整可用的类通常包括方法(也称为函数)和数据(也称为变量)。在调用时,通常使用一个点(.)来引用类中的变量。比如,我们只输出alice的年龄,则上面main()方法中的代码如下:

输出结果:

如果忘记初始化名为alice的对象,或无意中给null赋值,在调用时就会出现空指针异常。为了做非空判定,通常的做法如下:

为了简化上述操作,提供了?.的调用方法,同时需要手动设置默认值。如下所示:

上面代码的意思是,当调用alice.age时,如果alice不为空,则使用alice对象中age变量的值;否则,将20作为值使用。最后,如果不清楚某一个对象的类型,就可以通过runtimeType来判断,代码如下:

运行结果:

如此,可得到类名,从而得知其对象类型。
4.1.2 实例变量
现在把注意力集中在Person类,发现在开始时有三个变量声明,如下:

这三个变量就称为实例变量。在声明时,可以对其赋值,未被赋值过的实例变量的默认值是null。而被赋值的变量的赋值操作将在该类被实例化时发生,且发生在构造方法和初始化列表操作前。
有关构造方法和初始化列表的知识将在后面的小节中讲解,这里可以简单地认为赋值是类在实例化操作中的第一步。要强调的是,构造函数的执行顺序为初始化参数列表→父类的无名构造函数→本类的无名构造函数。
4.1.3 getter()方法和setter()方法
和某些面向对象的高级编程语言不同,Dart是不需要手动去写getter()方法和setter()方法的,它会自动为每一个实例变量生成getter()方法。对于非final修饰的实例变量,也会自动生成setter()方法。在使用时,可以直接以“对象.实例变量”的方式访问。同时,Dart也支持自定义getter()方法和setter()方法,方法使用get和set关键字。
接下来实现一个功能:当我们使用姓名、年龄和性别去初始化一个对象后,通过一个方法得到这个人的年龄阶段描述,如儿童、少年、青年,而这要根据年龄来判断。代码片段如下:

将上述代码放到Person类中,然后回到main()方法中调用它:

运行结果:

4.1.4 静态变量
静态变量,又称为类变量。和实例变量不同的是,静态变量是对于一个类而言的。在Person类中,声明一个静态变量的方法是使用static关键字,如下所示:

再回到main()方法中,当尝试用alice对象访问notice时,会发现根本无法访问,更不要说更改它了。因此,只能用Person类访问notice,如下所示:

和实例变量不同,静态变量在第一次使用时就需要初始化,即使没有创建任何该类的对象。
4.1.5 构造方法
除变量外,我们会发现Person类中还有一段代码:

这就是Person类的构造方法。由于在上面构造方法体中,利用参数给实例变量(下一节中会解释实例变量)赋值的场景非常常见,故Dart提供了简便写法,如下所示:

1.默认构造方法
默认构造方法很明显也是一个方法,只不过它和类的名字一样,小括号中的参数是可选的。在Dart中,如果一个类不包含任何构造方法,Dart就会自动添加一个没有任何参数的默认构造方法,如下所示:

因此,如果想定义默认构造方法,其实不用去写,因为Dart已经默认提供了。你可能会问,在构造方法的方法体中为什么要写this,它是什么意思?this关键字代表当前实例。根据Dart代码风格样式规范,实际上是不推荐使用this关键字的,但是在上面带有参数的构造方法中,如果直接忽略this,就会变成:

显然这是没有意义的。因此,这是借助this关键字来解决变量名冲突的问题。
2.命名构造方法
除了上述使用类名作为构造方法名的构造方法,还有命名构造方法。具体代码片段如下:

在main()方法中,调用这个构造方法需要创建对象:

运行结果:

当然,这里出于实际的代码逻辑考虑,仅仅要求一个参数,即年龄,因为名字和性别不会轻易更改。
3.调用父类的构造方法
当一个类作为子类存在时,它的构造方法会自动调用其父类的无名无参数的默认构造方法,调用的时机是在子类的构造方法开始执行之前。当父类没有无名构造方法时,则需要使用冒号(:)调用父类的其他构造方法。代码片段如下:
Person.dart代码:


main.dart代码:


运行结果:

Student类是Person类的子类,使用extends关键字表示继承关系。它调用了父类的myself构造方法,可以看到,最后输出的gender值为male,age为16,正是父类构造方法执行后的结果。在执行完父类构造方法后,才执行本类中的代码,将name的值赋为Student,并且在打印“I'm student, $age years old.”后结束自身的构造方法。因此,调用toString()方法输出了希望看到的结果。
在调用父类的构造方法前,还可以通过初始化列表来初始化变量的值。首先在Person类中添加下面的构造方法:

然后在main()方法中使用此构造方法进行实例化:

最后运行,结果如下:

4.重定向构造方法
对于一个类,有时候可能存在多个构造方法,而这些方法中可能存在某些相同的逻辑。为了使代码足够简洁和易于维护,我们可以把共同的部分提取出来,然后分别实现不同逻辑的部分即可。
对于上面的Person类,为了简化使用,在输入性别时,不再输入male和female,而是简单地输入0代表male,1代表female,但是要求在输出结果时按照male和female输出。此时,重定向构造方法便是一种解决途径。依然在Person类中加入构造方法,这一次按照如下写法来添加:

然后在main()方法中测试:

运行结果:

在easyGender的命名构造方法中,对0和1的性别输入进行相应的转换,然后调用其他的构造方法简化了重复赋值的操作。
5.常量构造方法
如果不允许Person类中的变量在实例化后随意更改,就要用到常量构造方法。常量构造方法使用const关键字,并声明所有类的变量为final。如上所述,在Person.dart中新建一个类:

然后在main()方法中实例化:

运行结果:

使用常量构造方法初始化的对象,其final修饰的变量无法再被更改。如

将会提示语法错误。
6.工厂方法的构造方法
前面所述的众多构造方法均会返回一个新的实例。为了减少硬件资源的消耗,Dart的设计者提供了工厂方法。所谓工厂方法,就是提供缓存,如果一个对象已经被实例化过,那么从缓存中取出来返回即可,不需要再生成一个新的对象。
因此,使用工厂方法不一定总是返回一个新的对象。也正因为如此,它可以减少实例化对象的时间。参考下面名为Person_2的新类:

在创建缓存区时使用了Map的组织形式,在类的内部使用了_cache对象。前面的String即key,用于保存某个人的姓名。只要有了姓名,就能找到他。然后,回到main()方法中写下如下代码:

运行结果:

通过调用对象.hashcode可以判断是不是两个不同的实例。如上所示,虽然sayHello和sayHello_2都是通过new Person_2去实例化的,但由于都使用了David作为缓存key,因此会得到同一个对象。而对于新来的Elan,由于_cache中没有Elan,因此将返回一个新的对象。
4.1.6 实例方法
实际上,我们已经多次使用过实例方法,如Person_2类中的say()方法。
Dart编程语言中的实例方法也是类成员方法,可以访问实例变量和this。因此,若要判断一个方法是不是实例方法,仅看这个方法体中能否使用实例变量和this关键字即可。
4.1.7 静态方法
和静态变量相似,静态方法也是对于整个类而言的,因此也被称为类方法,同时它也无法在类的实例上执行。举例来说,扩展之前的Person_2类如下:


注意,在最后的readme()方法前加了static关键字,这是一个静态方法。在使用静态方法时,只需要使用“类名.静态方法”即可。具体如下:

运行结果:

同样地,如果使用类的实例,如sayHello,调用readme()方法就会收到语法错误提示。
4.1.8 扩展类
所谓扩展类,实际上就是类的继承(使用extends关键字)及方法的复写(添加@Override注解)。不管是类的继承还是方法的复写,在之前的示例中,其实多多少少都有体现,下面我们就用一个实际案例来具体讲解。
想象这样的情况:要建立两个类来模拟手机的操作,其中一部是iPhone手机,另一部是Android手机。根据现有的知识,我们会写两个类分别对应两部手机的某些特点和操作。本节中要写三个类:其中一个类是父类,也叫作超类。这个类包含了所有手机的共同特点和功能,如屏幕、质量、打电话、发短信等;另外两个类对应实际的手机,包含手机的特有功能,如Android手机的品牌、iPhone手机的Power键功能等。
在这里,仅举例一部完整的手机中的少量特性和功能。首先新建一个父类,类名为MobilePhone,具体代码如下:


可以看到,父类的内容很简单:三个实例变量分别对应屏幕尺寸、手机质量和发布时间;打电话和发短信两个实例方法分别需要被叫号码、接收号码和短信内容。下面继续新建用来表示iPhone手机和Android手机的子类。

再回到main()方法中,分别实例化这两部不同的手机:


最后调用各自的toString()方法、call()方法和sendSms()方法,结果如下:

可见,虽然这两个类都继承了MobilePhone,但是又有各自的特色。对于MobilePhone中的变量和方法,就不需要再重复编码了。当然,你也可以自由地输出其他信息。
对于上例,如果我们不满足于单纯调用父类的方法,而是想在各自的方法中加上一个手机品牌信息,就要借助super关键字来复写父类的方法。具体的操作如下:
对于iPhone手机:

Android手机:


main()方法中的内容保持不变,运行结果如下:

如果不希望父类的方法被调用,就去掉super这一行。
4.1.9 可复写的运算符
运算符也可以被复写,但是并非所有的运算符都能复写。能够复写的运算符如表4.1所示。
表4.1 可复写的运算符

定义一个用于两项整数分别相乘的类,在其中复写乘号(*)运算符:

在main()方法中实例化两个对象,并让它们相乘:

运行结果:

可见,代码按照我们希望的逻辑运行,实现了两个整数分别相乘的需求。当然,你也可以尝试两个数交叉相乘,或者自定义其他运算符的功能。
4.1.10 抽象方法
在Dart中,支持抽象方法。它和实例方法不同,实例方法要求完整的方法名和方法体,即方法的实现;但是抽象方法则只要求方法名,方法体在其子类中实现。回到之前讲解继承的例子中,尝试将父类声明为抽象类,并将其中的打电话(call())方法改为一个抽象方法。完整的代码如下:


此时,两个子类均会在原有的super.call()方法上报语法错误,改正如下:
iPhone类:

AndroidPhone类:

main()方法中的内容保持不变,运行结果如下:

综上所述,抽象方法就是仅对方法进行声明,然后放到子类中具体实现。
4.1.11 抽象类
上例中,我们将名为MobilePhone的父类声明为抽象类,这样MobilePhone便成了一个抽象类。抽象类无法被实例化,这也就意味着无法通过new MobilePhone()方法来初始化一个对象。
抽象类一般会包含一个或多个抽象方法,同时也允许具体的实例方法存在。由于在上例中使用过抽象类,因此这里就不再重复举例。
4.1.12 接口
对于之前的例子,现在需要实现一个相同的功能,就是报出手机的品牌,但是具体的实现方法在另外一个类中。

由于Dart编程语言是不支持多个类继承的,因此在这种情况下,就要用到接口的概念。实现接口的关键字是 implememnts,我们采用和继承类似的写法改造iPhone类和AndroidPhone类。
iPhone类:


AndroidPhone类:

最后,在main()方法中分别调用两个类实例的printMyBrand()方法,得到结果:


上例中的玄机在于使用了implements。有了它,便有了方法实现的多样性,这对于上例中的应用场景十分合适。而且,在接口的实现上,Dart编程语言是支持多实现的。其结构如下:

4.1.13 利用Mixin特性扩展类
众所周知,Android和iOS在App后台运行的机制不同,Android可以提供几乎所有App的后台运行,而iOS只有定位、音乐播放等后台保持运行。因此,接下来,我们继续对AndroidPhone类进行扩充,添加一个将App放在后台运行的方法。和之前类似,后台运行也被放在另外一个类中。

由于这次不需要再重新实现它,因此使用implements就显得不太合适,这时就需要Mixin特性来救场。要使用Mixin特性就需要用到with关键字,后面紧跟着类名。特别注意的是,要将它们放在extends之后,implements之前。下面来看一下修改后的AndroidPhone类:


该类的首行使用了with关键字,而且在本类中并没有重写BackgroundApp()方法。接下来,在main()方法中调用:

运行结果:

可见,方法已经被添加到AndroidPhone类中并成功调用了。当然,作为with后的方法也是可以复写的,复写的原则和继承后的复写类似。
4.1.14 枚举
枚举,即enums或enumerations,是一种特殊的类。它通常用来表示具有固定数目的常量,但是它无法继承,无法使用Mixin特性,也无法实例化。先来看一个示例:

这是一个典型的枚举示例,用来表示Android设备的品牌。类似于列表,它的下标也是从0开始的,使用index可得到下标值:

在实际开发中,枚举可以帮助我们写出更易懂的代码。比如,要判断一款手机的品牌,仅需如下操作:

这样可减少由于拼写失误导致的异常,另外,当需要发生变化时,仅需对枚举类的内容进行修改即可,不需要在调用它的代码位置修改,降低了维护成本。