![Flutter从0基础到App上线](https://wfqqreader-1252317822.image.myqcloud.com/cover/259/33831259/b_33831259.jpg)
4.1 类
和其他面向对象的高级编程语言类似,Dart所有的类都是Object的子类,即继承于Object类。不同的是,Dart的基本数据类型属于对象,甚至null也属于对象。因此,可以说Dart是一种真正面向对象的语言。
4.1.1 类的实例化
首先来看下面一段代码:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_1.jpg?sign=1739348302-dQE1bakqb6aRGciaY6WG0wU4Nf6Z0kU9-0-543e8edc32131a8a7c82e541f2aa2161)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_2.jpg?sign=1739348302-j8Fac5MYZfudembH64iyeYHcMoYCd3B6-0-5c58d96cc78b610aad47deb2ef8e5577)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_3.jpg?sign=1739348302-UpLyDwmesVWtL1RE6O0gOP5KC1rJRoC5-0-b7f2be7e6236f20ad4bd4dc9c51d7705)
在上面的代码中,有一段代码和main()方法的缩进一样,即Person类。这个类是一个自定义的类,其大括号包含了这个类的实现。实际上,在使用String类的实例时,String作为基本数据类型,其类不需要自定义,直接拿来用即可,所以看不到String类的源码。在使用Person类时,可以把它当作String类看待。
在定义类时,我们可以像上面的例子一样直接把多个类写到一个文件中,也可以用单独的一个文件来实现一个类。例如,对于上例而言,新建一个文件,命名为Person.dart,把Person类的具体内容写到这个文件中,然后删掉原先的Person类实现。由于删掉了Person类的实现内容,原先的代码会报错。此时,需要指明要使用的类,即import(导入)Person类。最终的代码将如下所示:
.dart代码:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_4.jpg?sign=1739348302-TgfoKpl4T4h6pQrfMf8aP9eX7jKEbVhg-0-7c42ac90f58c3a0fbd4d3449e7b5608b)
Person.dart代码:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_5.jpg?sign=1739348302-ttwGPgTpa3YyFS5WDB6dzrLCH9HCgs4N-0-a1a82a04854a7d930f9ae19868998250)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_6.jpg?sign=1739348302-SbM5UsTcv7gjKjhEo8FhEcans1Z6N8l2-0-5c500c29ab652f88d4695315dbdbb9cd)
运行结果和前面的相同。通常意义上讲,为了确保代码的可读性和可维护性,会将不同的类放在不同的文件中单独处理。一个完整可用的类通常包括方法(也称为函数)和数据(也称为变量)。在调用时,通常使用一个点(.)来引用类中的变量。比如,我们只输出alice的年龄,则上面main()方法中的代码如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_7.jpg?sign=1739348302-vC68D8JFOwVRX1V9vCcWdDOFB6ZW90wB-0-41511efc1eca3bc689d3a6cf77ca6597)
输出结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_8.jpg?sign=1739348302-hsZ79T9MIoim8F55rnNqgw0xCpptWBNs-0-25da6151dd9f0b7817c7260c77efc9ae)
如果忘记初始化名为alice的对象,或无意中给null赋值,在调用时就会出现空指针异常。为了做非空判定,通常的做法如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_9.jpg?sign=1739348302-1996Fxm3PYhbJM25aoJ2ENDL8X63oEW3-0-45f1cee9fe13c6c644979d21dc392304)
为了简化上述操作,提供了?.的调用方法,同时需要手动设置默认值。如下所示:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_10.jpg?sign=1739348302-gOBQKhth1j4FzalZmUkGCabk1V9cWG9Y-0-6674440a08f25074df41704066e52b21)
上面代码的意思是,当调用alice.age时,如果alice不为空,则使用alice对象中age变量的值;否则,将20作为值使用。最后,如果不清楚某一个对象的类型,就可以通过runtimeType来判断,代码如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_11.jpg?sign=1739348302-QSeoE6S1GIPNjwI0G6AH3OT4Js5PdSHk-0-6937a70123136df508c8d241b95b7c88)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_12.jpg?sign=1739348302-ggLal9ZvFbUPvOWX04ugloiLbIKTt3Pg-0-59cbfec0eab2f4d7201b574c69120b54)
如此,可得到类名,从而得知其对象类型。
4.1.2 实例变量
现在把注意力集中在Person类,发现在开始时有三个变量声明,如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_13.jpg?sign=1739348302-g4lnrKkcJ8CmAmVQjOegLovXNRsvdbsn-0-8c35da46736b6de3ea15b78f2a0bc869)
这三个变量就称为实例变量。在声明时,可以对其赋值,未被赋值过的实例变量的默认值是null。而被赋值的变量的赋值操作将在该类被实例化时发生,且发生在构造方法和初始化列表操作前。
有关构造方法和初始化列表的知识将在后面的小节中讲解,这里可以简单地认为赋值是类在实例化操作中的第一步。要强调的是,构造函数的执行顺序为初始化参数列表→父类的无名构造函数→本类的无名构造函数。
4.1.3 getter()方法和setter()方法
和某些面向对象的高级编程语言不同,Dart是不需要手动去写getter()方法和setter()方法的,它会自动为每一个实例变量生成getter()方法。对于非final修饰的实例变量,也会自动生成setter()方法。在使用时,可以直接以“对象.实例变量”的方式访问。同时,Dart也支持自定义getter()方法和setter()方法,方法使用get和set关键字。
接下来实现一个功能:当我们使用姓名、年龄和性别去初始化一个对象后,通过一个方法得到这个人的年龄阶段描述,如儿童、少年、青年,而这要根据年龄来判断。代码片段如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_14.jpg?sign=1739348302-X29DiXxJNBKgIcBDLvBnRrdzuSImkdvn-0-c9639ba9cdef05c6ea32ce0cbe23c5aa)
将上述代码放到Person类中,然后回到main()方法中调用它:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_15.jpg?sign=1739348302-iFSd7hGkND4revISaYaoBvmSmOI0U5nW-0-b8eb6e97a48b55161ccc75c3fdb3def1)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_16.jpg?sign=1739348302-1Lfj1wZVFlZT0YhQFvX5rBmsCJrkVEHu-0-e995da0d708d51a6dd0f981468118b5d)
4.1.4 静态变量
静态变量,又称为类变量。和实例变量不同的是,静态变量是对于一个类而言的。在Person类中,声明一个静态变量的方法是使用static关键字,如下所示:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_17.jpg?sign=1739348302-STsXsgbKb8aLoOEcB4u2txJvbyHrc97C-0-9164d1a2c11cfea93675bf5614a30069)
再回到main()方法中,当尝试用alice对象访问notice时,会发现根本无法访问,更不要说更改它了。因此,只能用Person类访问notice,如下所示:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_18.jpg?sign=1739348302-xh1VCs08pAuAKraiBlq1WFQ6Rfh1uK7G-0-58045fe74d6b6137f76be8f0a00e9a20)
和实例变量不同,静态变量在第一次使用时就需要初始化,即使没有创建任何该类的对象。
4.1.5 构造方法
除变量外,我们会发现Person类中还有一段代码:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_19.jpg?sign=1739348302-CDOwmQFfyabaXaN5ymETFsw4pSGTTf4i-0-11a346f11d0d97fce2a184016f8abf1b)
这就是Person类的构造方法。由于在上面构造方法体中,利用参数给实例变量(下一节中会解释实例变量)赋值的场景非常常见,故Dart提供了简便写法,如下所示:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_20.jpg?sign=1739348302-a3p1LkFd4DQovoXAeu7n5G8F6wzz9jUj-0-6117be4ca3568b3f29cc50ce799bb023)
1.默认构造方法
默认构造方法很明显也是一个方法,只不过它和类的名字一样,小括号中的参数是可选的。在Dart中,如果一个类不包含任何构造方法,Dart就会自动添加一个没有任何参数的默认构造方法,如下所示:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_21.jpg?sign=1739348302-tkfo35n9MJHs7Nd0FwXgb61t4rotaSff-0-d73b263d9811844833e81bc29aebe57a)
因此,如果想定义默认构造方法,其实不用去写,因为Dart已经默认提供了。你可能会问,在构造方法的方法体中为什么要写this,它是什么意思?this关键字代表当前实例。根据Dart代码风格样式规范,实际上是不推荐使用this关键字的,但是在上面带有参数的构造方法中,如果直接忽略this,就会变成:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_22.jpg?sign=1739348302-cs1yJk8DSGILRmrg0JAmmt2OPAuXkw2I-0-0ce4bca80437b41a977c86e05758784b)
显然这是没有意义的。因此,这是借助this关键字来解决变量名冲突的问题。
2.命名构造方法
除了上述使用类名作为构造方法名的构造方法,还有命名构造方法。具体代码片段如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_23.jpg?sign=1739348302-ZeB5w7pCaHhyzuO6TDh13mC2kzs9lvUg-0-ac3b6dd4a357c15db4a574ab816625f9)
在main()方法中,调用这个构造方法需要创建对象:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_24.jpg?sign=1739348302-g3JN9Q39GMNgl8q6e5SoAQmHRNN1gwNV-0-03d45e2f021897993fb161e07e91d7fb)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_25.jpg?sign=1739348302-g1uiYjx8qVemjEs4hbv6fFTQ954orC4a-0-b15380eb7f7849c7de8fd972c7ec8e04)
当然,这里出于实际的代码逻辑考虑,仅仅要求一个参数,即年龄,因为名字和性别不会轻易更改。
3.调用父类的构造方法
当一个类作为子类存在时,它的构造方法会自动调用其父类的无名无参数的默认构造方法,调用的时机是在子类的构造方法开始执行之前。当父类没有无名构造方法时,则需要使用冒号(:)调用父类的其他构造方法。代码片段如下:
Person.dart代码:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_26.jpg?sign=1739348302-u6VYjcpFg6K7t1rzFzC45zyAciku3kk4-0-332dfec780554da3adbe680514d48228)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_27.jpg?sign=1739348302-xxFjRIgESU78U2pU8MEFRPCf9Dtzovfl-0-0bf5d52707961c56431aabe63677f221)
main.dart代码:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_28.jpg?sign=1739348302-1ippqNOcZIGA2GYM0orkFhU3o8kdyeUc-0-b339957e15e61ff2abe31dccfc16650c)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_29.jpg?sign=1739348302-7vXhyhNWIUjcYBYSPsTukWFacuTTECkn-0-5faff4a164fc2009f94c8788f782dd75)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_30.jpg?sign=1739348302-zjTyOOYZ9BedfXp4W7q9TTMH2RhQwEk6-0-264f3ea5ee3f696424c9a05cd5765c1d)
Student类是Person类的子类,使用extends关键字表示继承关系。它调用了父类的myself构造方法,可以看到,最后输出的gender值为male,age为16,正是父类构造方法执行后的结果。在执行完父类构造方法后,才执行本类中的代码,将name的值赋为Student,并且在打印“I'm student, $age years old.”后结束自身的构造方法。因此,调用toString()方法输出了希望看到的结果。
在调用父类的构造方法前,还可以通过初始化列表来初始化变量的值。首先在Person类中添加下面的构造方法:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_31.jpg?sign=1739348302-WuGRh8MNp12qPE0VHgcDtvvuCAJR54RW-0-49c8f998cfbc271cd29fb94ef212f234)
然后在main()方法中使用此构造方法进行实例化:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_32.jpg?sign=1739348302-31tXdpAA6xIcXVWKvFtwJWW1AAiTYfOI-0-5c306876ef8303c03cf31e80f78d5ac2)
最后运行,结果如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_33.jpg?sign=1739348302-CpuN240EFZFJTDnwYTdWQXBg2Z6LOJx3-0-e76f602563ff481678e90a43e34d4be4)
4.重定向构造方法
对于一个类,有时候可能存在多个构造方法,而这些方法中可能存在某些相同的逻辑。为了使代码足够简洁和易于维护,我们可以把共同的部分提取出来,然后分别实现不同逻辑的部分即可。
对于上面的Person类,为了简化使用,在输入性别时,不再输入male和female,而是简单地输入0代表male,1代表female,但是要求在输出结果时按照male和female输出。此时,重定向构造方法便是一种解决途径。依然在Person类中加入构造方法,这一次按照如下写法来添加:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_34.jpg?sign=1739348302-XSStPM7rQ4RMYZLrTK10EQCi4vIrM2U4-0-309fc13820256879d074bfacf5bda256)
然后在main()方法中测试:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_35.jpg?sign=1739348302-ZCnqJk1AfUglgrM3T0e7xjhlYFR5PusM-0-794d7b007dbfa9c5d79d85a4b6530b89)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_36.jpg?sign=1739348302-jUejCSXXkpfHg5ONyVTMcGKtzaymlxJ0-0-4046e9eed6124d78814084e410243822)
在easyGender的命名构造方法中,对0和1的性别输入进行相应的转换,然后调用其他的构造方法简化了重复赋值的操作。
5.常量构造方法
如果不允许Person类中的变量在实例化后随意更改,就要用到常量构造方法。常量构造方法使用const关键字,并声明所有类的变量为final。如上所述,在Person.dart中新建一个类:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_37.jpg?sign=1739348302-xnUZDO5AKjcof69s4rztJSlo8nTNwkVA-0-940768be40f6c96cc1a05ecc21033c9c)
然后在main()方法中实例化:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_38.jpg?sign=1739348302-xoez3aTjL5OUXMqAVPbmScdlgCA1Am1u-0-4c690141cd5e6ca93eff3f24a4dc8f04)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_39.jpg?sign=1739348302-0eRcgm2vMBXNaIcFnGUt31IFmF9R8oxC-0-0604dfbe77bfdbacf4f77f1669fd45f6)
使用常量构造方法初始化的对象,其final修饰的变量无法再被更改。如
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_40.jpg?sign=1739348302-yuTsFcq7Rk0lOUayazCCXZz4yN6kfQ4v-0-f343e3840e4cfb5daba5f08d02a72083)
将会提示语法错误。
6.工厂方法的构造方法
前面所述的众多构造方法均会返回一个新的实例。为了减少硬件资源的消耗,Dart的设计者提供了工厂方法。所谓工厂方法,就是提供缓存,如果一个对象已经被实例化过,那么从缓存中取出来返回即可,不需要再生成一个新的对象。
因此,使用工厂方法不一定总是返回一个新的对象。也正因为如此,它可以减少实例化对象的时间。参考下面名为Person_2的新类:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_41.jpg?sign=1739348302-BKneAKALANIqmrpNiAM9l863xjwU6QOU-0-7a20e37a3cc758c1b206eadda9c3a9f4)
在创建缓存区时使用了Map的组织形式,在类的内部使用了_cache对象。前面的String即key,用于保存某个人的姓名。只要有了姓名,就能找到他。然后,回到main()方法中写下如下代码:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_42.jpg?sign=1739348302-vBaMVU584VXuM7wjpT4t6mVuEkbzvPFs-0-8b04f5c63aeb922c8351c334e056acc1)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_43.jpg?sign=1739348302-IBnQjdyocOmaLBgXhgnqlpUKllTP2VPB-0-270d4cd1c49f0034af7ee61e67ef43dd)
通过调用对象.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类如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_44.jpg?sign=1739348302-Ilsy9P7DSb2SLx0UiJjEMBEucqJybgBS-0-cb4a46b7614eac97eb6586990d0ada34)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_45.jpg?sign=1739348302-laSGjGvwSRfdPijCSpRIk7nnaOs0CZZ8-0-5a48981b388064b479d876fc2e188f22)
注意,在最后的readme()方法前加了static关键字,这是一个静态方法。在使用静态方法时,只需要使用“类名.静态方法”即可。具体如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_46.jpg?sign=1739348302-l4H4qr1yGWPLsxfTJukt5km0UDKi3ntF-0-be5a6a71300229837c868062bd4da41f)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_47.jpg?sign=1739348302-hXTg6QvhpZ9jkzXH5j8adTiB28U2iPHM-0-18afca5509e2d2f06eb3429f0654967c)
同样地,如果使用类的实例,如sayHello,调用readme()方法就会收到语法错误提示。
4.1.8 扩展类
所谓扩展类,实际上就是类的继承(使用extends关键字)及方法的复写(添加@Override注解)。不管是类的继承还是方法的复写,在之前的示例中,其实多多少少都有体现,下面我们就用一个实际案例来具体讲解。
想象这样的情况:要建立两个类来模拟手机的操作,其中一部是iPhone手机,另一部是Android手机。根据现有的知识,我们会写两个类分别对应两部手机的某些特点和操作。本节中要写三个类:其中一个类是父类,也叫作超类。这个类包含了所有手机的共同特点和功能,如屏幕、质量、打电话、发短信等;另外两个类对应实际的手机,包含手机的特有功能,如Android手机的品牌、iPhone手机的Power键功能等。
在这里,仅举例一部完整的手机中的少量特性和功能。首先新建一个父类,类名为MobilePhone,具体代码如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_48.jpg?sign=1739348302-pKsIx0JlIimYsAEUMn0Q1EMfkJSlNW7s-0-7d9e5879c03938931032899cdd7a7cb7)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_49.jpg?sign=1739348302-uaIv1X2gAl7TjaLkHv5VRCjS1JsSbsTA-0-7bc47776a7ce0270c188b6a5ccc42295)
可以看到,父类的内容很简单:三个实例变量分别对应屏幕尺寸、手机质量和发布时间;打电话和发短信两个实例方法分别需要被叫号码、接收号码和短信内容。下面继续新建用来表示iPhone手机和Android手机的子类。
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_50.jpg?sign=1739348302-BGEigGwYMMEeqV8YIhvPSrG0w22pe6De-0-e283189cd5d4d2194ddafd5e0e5e04cb)
再回到main()方法中,分别实例化这两部不同的手机:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_51.jpg?sign=1739348302-rcu0ECFO8Ogk0Y7xg21whVVvsjq8ke7D-0-f9ca35edc80d200bcfcae08da13a5190)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_52.jpg?sign=1739348302-jfpGfBMNWS12mELLC8W5YNukD5ig2oXa-0-24cfd351010ac45598bf39e1eee4a36d)
最后调用各自的toString()方法、call()方法和sendSms()方法,结果如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_53.jpg?sign=1739348302-dWpFO10hP5a5ZX0xyMy7s33zPJxCfda8-0-5620f2dd2421251e4c55585c316c21a6)
可见,虽然这两个类都继承了MobilePhone,但是又有各自的特色。对于MobilePhone中的变量和方法,就不需要再重复编码了。当然,你也可以自由地输出其他信息。
对于上例,如果我们不满足于单纯调用父类的方法,而是想在各自的方法中加上一个手机品牌信息,就要借助super关键字来复写父类的方法。具体的操作如下:
对于iPhone手机:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_54.jpg?sign=1739348302-Kzk5W0IGqnosOJU0ysQewkraUcakg0dd-0-2d5859310ca1146a63182bc6d58e691b)
Android手机:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_55.jpg?sign=1739348302-nSsatTQQt81uouMAL3y0SrU33yaiAjEB-0-20de088a2fe0baffaafa20d02bac9554)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_56.jpg?sign=1739348302-C0yHh2sZq9oUB75V0cJ5RwqyialRkfwu-0-586588b93151fbf162ee7a90922f5f0c)
main()方法中的内容保持不变,运行结果如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_57.jpg?sign=1739348302-YhAi0tKPYZiW6d5MpLGt14X5JIH6dZ9K-0-4db9dc68ae7ff0651d8e721f6ea3c622)
如果不希望父类的方法被调用,就去掉super这一行。
4.1.9 可复写的运算符
运算符也可以被复写,但是并非所有的运算符都能复写。能够复写的运算符如表4.1所示。
表4.1 可复写的运算符
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_58.jpg?sign=1739348302-JeuVe815j1iZNeLo2TnzeabGVcL2gkVq-0-72a5b1a70d6e31c1af26fe7dea9b2df1)
定义一个用于两项整数分别相乘的类,在其中复写乘号(*)运算符:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_59.jpg?sign=1739348302-5C0Zd2Qrxr6zopxr73iwAfjTXovIC5mf-0-a74fb470995bae400f46d42602d296e2)
在main()方法中实例化两个对象,并让它们相乘:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_60.jpg?sign=1739348302-ivZiCevWc6F1jPNK69LZK7D9lf0cLHuF-0-b020f3b7ef39f93316099b9f9a13941e)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_61.jpg?sign=1739348302-YkEjs1TSLKZfZwFtAteH4yen6W0VnrZl-0-2cb74f1a7a70786fd2fdc3f82a77d371)
可见,代码按照我们希望的逻辑运行,实现了两个整数分别相乘的需求。当然,你也可以尝试两个数交叉相乘,或者自定义其他运算符的功能。
4.1.10 抽象方法
在Dart中,支持抽象方法。它和实例方法不同,实例方法要求完整的方法名和方法体,即方法的实现;但是抽象方法则只要求方法名,方法体在其子类中实现。回到之前讲解继承的例子中,尝试将父类声明为抽象类,并将其中的打电话(call())方法改为一个抽象方法。完整的代码如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_62.jpg?sign=1739348302-pVWTcUeGjVLsRzkk2NS4d7LUPq6utRyJ-0-aa02cc2d68080f83b2e7b270d022b5dc)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_63.jpg?sign=1739348302-D5IvVnd51HThqSpgvGCiLmAjBEef0gc8-0-86bf8a25e178b13026baf7391cb35b87)
此时,两个子类均会在原有的super.call()方法上报语法错误,改正如下:
iPhone类:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_64.jpg?sign=1739348302-VwDeOFaFzhJ1CcpGAPx4mvleAuIKvAa8-0-c2372f136b653ff8f0ac9b55945a7dbe)
AndroidPhone类:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_65.jpg?sign=1739348302-cw2cJlLmkd0YUwXCfTGSHI5rVdY1Va9N-0-cb75f9248b96c52025ce930e6ed1e65b)
main()方法中的内容保持不变,运行结果如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_66.jpg?sign=1739348302-8zgLvwm5oqtylYi4WJQgvxYdyVbQJBj5-0-5ae3b218b59e0201ebdc6d9afc9f5db4)
综上所述,抽象方法就是仅对方法进行声明,然后放到子类中具体实现。
4.1.11 抽象类
上例中,我们将名为MobilePhone的父类声明为抽象类,这样MobilePhone便成了一个抽象类。抽象类无法被实例化,这也就意味着无法通过new MobilePhone()方法来初始化一个对象。
抽象类一般会包含一个或多个抽象方法,同时也允许具体的实例方法存在。由于在上例中使用过抽象类,因此这里就不再重复举例。
4.1.12 接口
对于之前的例子,现在需要实现一个相同的功能,就是报出手机的品牌,但是具体的实现方法在另外一个类中。
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_67.jpg?sign=1739348302-Rt8zEcBbK5J4vHKi2DyvIl46lCw59w9K-0-1387e8c2943fb509c0ecd525d7c8226a)
由于Dart编程语言是不支持多个类继承的,因此在这种情况下,就要用到接口的概念。实现接口的关键字是 implememnts,我们采用和继承类似的写法改造iPhone类和AndroidPhone类。
iPhone类:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_68.jpg?sign=1739348302-2Eso3amCETnh2EoKUhYrbOa5f9tWQW1B-0-9aa699e616d34770aacfbfd89b0456dd)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_69.jpg?sign=1739348302-POcYIqOWzoqL7EHtwIUMU7gBhwYqiY1b-0-7083b1f3f989b5e1a4eff5a2fd2cc549)
AndroidPhone类:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_70.jpg?sign=1739348302-YsW5DR9sTy1h67zgrl1SonLJFeYWdC1T-0-ee723c87ee358a3737b82caa21ab94a7)
最后,在main()方法中分别调用两个类实例的printMyBrand()方法,得到结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_71.jpg?sign=1739348302-2S7dicdz9MpVV87ipjPgL01uj8WFmCWj-0-fa2e532f41b782c1f6cb8f37eafb52d7)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_72.jpg?sign=1739348302-BHKZdvRC4x6wqlhYeKLSbQEaPlpA49B2-0-38696ceb92a1829e8fb6b9162f04f137)
上例中的玄机在于使用了implements。有了它,便有了方法实现的多样性,这对于上例中的应用场景十分合适。而且,在接口的实现上,Dart编程语言是支持多实现的。其结构如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_73.jpg?sign=1739348302-FXS1OrU9T54QwCDNuYNqaKlbB0PwrnXU-0-37315f871e4e79ad580237806e59367a)
4.1.13 利用Mixin特性扩展类
众所周知,Android和iOS在App后台运行的机制不同,Android可以提供几乎所有App的后台运行,而iOS只有定位、音乐播放等后台保持运行。因此,接下来,我们继续对AndroidPhone类进行扩充,添加一个将App放在后台运行的方法。和之前类似,后台运行也被放在另外一个类中。
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_74.jpg?sign=1739348302-sljRZzxi1fqIrLhweLQW1NMPTW692dHt-0-42eeb287cfc0fe7f93e548f3751ba3eb)
由于这次不需要再重新实现它,因此使用implements就显得不太合适,这时就需要Mixin特性来救场。要使用Mixin特性就需要用到with关键字,后面紧跟着类名。特别注意的是,要将它们放在extends之后,implements之前。下面来看一下修改后的AndroidPhone类:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_75.jpg?sign=1739348302-hswrG8tbARctWkqFGoDpdzN8JvNN6bzp-0-e9f708768320d7add0356446daac2cab)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_76.jpg?sign=1739348302-OE0d0yYaEhzJCpXxP9cJPuagaNvlXptw-0-00e06eca1451d242730689680ec2752d)
该类的首行使用了with关键字,而且在本类中并没有重写BackgroundApp()方法。接下来,在main()方法中调用:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_77.jpg?sign=1739348302-1lMmCDdL1C8BxaCuVqFocm6Yr4TZakU4-0-87e631a1cb2aaf1a20430f184a8d0387)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_78.jpg?sign=1739348302-kPV93D2VQdFNp4Kmyk2hlmO6aaybzF1s-0-891d8f402c4406b5541809dde21dbe5d)
可见,方法已经被添加到AndroidPhone类中并成功调用了。当然,作为with后的方法也是可以复写的,复写的原则和继承后的复写类似。
4.1.14 枚举
枚举,即enums或enumerations,是一种特殊的类。它通常用来表示具有固定数目的常量,但是它无法继承,无法使用Mixin特性,也无法实例化。先来看一个示例:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_79.jpg?sign=1739348302-K7ppQXTExL78BM0YUhdR76TcJir12t7n-0-593d1901d2acb7456c9537a0c8d0c034)
这是一个典型的枚举示例,用来表示Android设备的品牌。类似于列表,它的下标也是从0开始的,使用index可得到下标值:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_80.jpg?sign=1739348302-seRiVDsu14yHOCZU1sNfoQOI1jxFMLau-0-edec4eaf996876106d8828cbd39c8baa)
在实际开发中,枚举可以帮助我们写出更易懂的代码。比如,要判断一款手机的品牌,仅需如下操作:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_81.jpg?sign=1739348302-EsIw9mwKFJs9AkA4B4SUSMmXyUZ66AyS-0-68cbb9163d4b499f0549165b2f2dfcae)
这样可减少由于拼写失误导致的异常,另外,当需要发生变化时,仅需对枚举类的内容进行修改即可,不需要在调用它的代码位置修改,降低了维护成本。