![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=1739282062-Nv1P0KR9Y9EzOYXs5zd2AjNn5weZ58h9-0-984156da8f15a1a762be9b11efdf0882)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_2.jpg?sign=1739282062-qTx5hy1wUDwp0cQyOWigIhTpYQQFh7cD-0-7b930449b8aac7f8ae461b2c9ffa0d60)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_3.jpg?sign=1739282062-kaginBdOHzjTUFJ6t015V54vRrqz9RQk-0-930a7f9004b3fce83e02fc5c6f23cc3e)
在上面的代码中,有一段代码和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=1739282062-03mcusuznmXQiVWtiFpiePDsMF09QuAw-0-199193719bca2edc81b3c4aa8035c9d9)
Person.dart代码:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_5.jpg?sign=1739282062-eSTB11G5D2w6BnuUlUaszSpZc5DudjBL-0-a8213cef5ef2bb602136a18146cbad07)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_6.jpg?sign=1739282062-npDCuPJtzU5zZg8w6P5aDCiEjd8Ob1zu-0-3be1ee55ee66423d7532f975a5892ace)
运行结果和前面的相同。通常意义上讲,为了确保代码的可读性和可维护性,会将不同的类放在不同的文件中单独处理。一个完整可用的类通常包括方法(也称为函数)和数据(也称为变量)。在调用时,通常使用一个点(.)来引用类中的变量。比如,我们只输出alice的年龄,则上面main()方法中的代码如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_7.jpg?sign=1739282062-SEoYBrtWLd1xdQsDabKTVODxapwKQers-0-9e10f666a6ea2e24ed74696eebc3d318)
输出结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_8.jpg?sign=1739282062-BZTciJmOWrzATnCNrSW3hgLxB4cdWqyn-0-da4001f8ae2f1b1e8a50e4adba2f1725)
如果忘记初始化名为alice的对象,或无意中给null赋值,在调用时就会出现空指针异常。为了做非空判定,通常的做法如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_9.jpg?sign=1739282062-6EVZWYcYmxem2KWvMp2epmVH9fOXIkXB-0-3e625b168eb9633943ff9caa28f2de6a)
为了简化上述操作,提供了?.的调用方法,同时需要手动设置默认值。如下所示:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_10.jpg?sign=1739282062-uf0hXyKgRwlstDhbsGNZclfQCl5XZVhq-0-e4b3e5a0aa72ec1cb667bb6bec50590c)
上面代码的意思是,当调用alice.age时,如果alice不为空,则使用alice对象中age变量的值;否则,将20作为值使用。最后,如果不清楚某一个对象的类型,就可以通过runtimeType来判断,代码如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_11.jpg?sign=1739282062-cDNxDxMJQLNrLqmkBi2053t20a4NnHcZ-0-4e89a56be63d069e3a7056f12c9a93ba)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_12.jpg?sign=1739282062-APFN9nm8RlgqnvB4HvBAYr6DnSkFuFB5-0-b0c1b475db084d415afa43f33e9dfae6)
如此,可得到类名,从而得知其对象类型。
4.1.2 实例变量
现在把注意力集中在Person类,发现在开始时有三个变量声明,如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_13.jpg?sign=1739282062-neQopefrGz8b2TRpCp50DMlJKDe38KT1-0-9dafb83f9130709a3558a391dae0190d)
这三个变量就称为实例变量。在声明时,可以对其赋值,未被赋值过的实例变量的默认值是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=1739282062-ULb81Ir0dKbwXGIqrPwMCBztUVgbybQL-0-43b64d793808120bc95b56acc44a4fd3)
将上述代码放到Person类中,然后回到main()方法中调用它:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_15.jpg?sign=1739282062-ys0m4WtRHBcyFK7TFbrAeDTfbqmHxbfH-0-951d2caaf8c6e661d4893eaed15acf86)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_16.jpg?sign=1739282062-efqxY0MSJv9pxekDi6a64K9reHiNBUGr-0-fa8d5a7d9ceada3e40f3eb9f42851cfc)
4.1.4 静态变量
静态变量,又称为类变量。和实例变量不同的是,静态变量是对于一个类而言的。在Person类中,声明一个静态变量的方法是使用static关键字,如下所示:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_17.jpg?sign=1739282062-SdtShlUGSNbrBbsohYDGPm5v1hAJlTUl-0-7d0f9543c2efe01f9787985d46051fef)
再回到main()方法中,当尝试用alice对象访问notice时,会发现根本无法访问,更不要说更改它了。因此,只能用Person类访问notice,如下所示:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_18.jpg?sign=1739282062-ydo131ABLSyi6bPc1I3DDdqmnosdwvgs-0-d40442d1d2433ae4acbdb51b11952c4c)
和实例变量不同,静态变量在第一次使用时就需要初始化,即使没有创建任何该类的对象。
4.1.5 构造方法
除变量外,我们会发现Person类中还有一段代码:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_19.jpg?sign=1739282062-9E5SggAyDyLgBsW1bFCQC3bLUiNbFsTr-0-7cf9b6e54ad781ec9495100cdad9472a)
这就是Person类的构造方法。由于在上面构造方法体中,利用参数给实例变量(下一节中会解释实例变量)赋值的场景非常常见,故Dart提供了简便写法,如下所示:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_20.jpg?sign=1739282062-TriavIt3LtULnrWjDOFUHsm8n337ZoYV-0-fef7a7e7e1bd647c738853eea3e7f82c)
1.默认构造方法
默认构造方法很明显也是一个方法,只不过它和类的名字一样,小括号中的参数是可选的。在Dart中,如果一个类不包含任何构造方法,Dart就会自动添加一个没有任何参数的默认构造方法,如下所示:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_21.jpg?sign=1739282062-tTkyijoBozRlKUdE5OJN9531RCgmB5so-0-6006e2fb271972c3b70c0fd7ec3b8e02)
因此,如果想定义默认构造方法,其实不用去写,因为Dart已经默认提供了。你可能会问,在构造方法的方法体中为什么要写this,它是什么意思?this关键字代表当前实例。根据Dart代码风格样式规范,实际上是不推荐使用this关键字的,但是在上面带有参数的构造方法中,如果直接忽略this,就会变成:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_22.jpg?sign=1739282062-vFno3Kb6r2SG8EsRtZKzvAeCxWX2kg7b-0-9257d94b882124b009a0fd6930afeea4)
显然这是没有意义的。因此,这是借助this关键字来解决变量名冲突的问题。
2.命名构造方法
除了上述使用类名作为构造方法名的构造方法,还有命名构造方法。具体代码片段如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_23.jpg?sign=1739282062-UlTnxyXLjvRUbiUyCWkDk7m6f0th3inP-0-5de250ba41823700b896f5103a95f6e7)
在main()方法中,调用这个构造方法需要创建对象:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_24.jpg?sign=1739282062-bwzYUIJIXSxHIcwLW3vIA1jkcGsUbjRK-0-91a7a5d9767bbbec2adcd5268089f48a)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_25.jpg?sign=1739282062-A52vA2Z213wy7chftuKuN4jZlTEGcBIR-0-5f64f113c0070900e2ef45097b524818)
当然,这里出于实际的代码逻辑考虑,仅仅要求一个参数,即年龄,因为名字和性别不会轻易更改。
3.调用父类的构造方法
当一个类作为子类存在时,它的构造方法会自动调用其父类的无名无参数的默认构造方法,调用的时机是在子类的构造方法开始执行之前。当父类没有无名构造方法时,则需要使用冒号(:)调用父类的其他构造方法。代码片段如下:
Person.dart代码:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_26.jpg?sign=1739282062-7XJvQ39TyAWUapqy1FeNk5UlVIOqgJTH-0-22c321240591dfa9cdff811e9d1d1802)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_27.jpg?sign=1739282062-eaPw2bU4hiEdp8tM33Z4swefqhKMyHfz-0-f8ef417d9440d161a0d7100f225e79e9)
main.dart代码:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_28.jpg?sign=1739282062-JnrvU8kRo6GWzGxVhufCRbicMT3MzMT2-0-9b7ce8cd11342bd4a5dd1bf41fceef08)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_29.jpg?sign=1739282062-PbKnZa215gKi9d06rCBXWmWoA9KQuvNZ-0-ba1c3522c8660639a87299e2938cddc7)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_30.jpg?sign=1739282062-wZUbSYqglXznf9PgWHUFiO2LToKHDDsQ-0-7e929e89321682bd72ca6d6c7dca69df)
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=1739282062-BHYj8i289u75tr8cZB2If2RWrOtrumIA-0-93c5756d48400eb9a692dcb9870e585c)
然后在main()方法中使用此构造方法进行实例化:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_32.jpg?sign=1739282062-9rCgulGrpXkWWoH9lG3FoVeb05xPGxNA-0-79163694582e0388b0835d2a1a8d8e3b)
最后运行,结果如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_33.jpg?sign=1739282062-zPIf87jEEiDaSc22qNzwOg7c7dEaGr50-0-6127c7d6b353f681abcf751693379584)
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=1739282062-Ivr6r67qZ8AlvIB3rGf6KX6ku7qeM0dK-0-8fafb2e14942a93ffb1b1f5f2fdd4661)
然后在main()方法中测试:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_35.jpg?sign=1739282062-yOdI2cOIReSFEyXaB6UIIzhSqiKhSmuj-0-9ff7598c5a134df7072a1bb349802427)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_36.jpg?sign=1739282062-jBfwu0MxnGExcrjQjEazkRX1UjKHVgJ4-0-17f595e60ba650974d4bee6c7426e815)
在easyGender的命名构造方法中,对0和1的性别输入进行相应的转换,然后调用其他的构造方法简化了重复赋值的操作。
5.常量构造方法
如果不允许Person类中的变量在实例化后随意更改,就要用到常量构造方法。常量构造方法使用const关键字,并声明所有类的变量为final。如上所述,在Person.dart中新建一个类:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_37.jpg?sign=1739282062-21hf4sdZ719DM6M17pSeMZxAAvKSu4fb-0-dc60a34eb15d04242d438c40b5935619)
然后在main()方法中实例化:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_38.jpg?sign=1739282062-o9j47XPWIFj255J1BSjJBHtkjSk8qmy1-0-a767d52a4d604522a610d41c498f4c50)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_39.jpg?sign=1739282062-Ho44S3XFq1jAeyOkB502dJwNJyCE1dBY-0-c7f1caa1ae22e94bf33ecf806c406bc9)
使用常量构造方法初始化的对象,其final修饰的变量无法再被更改。如
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_40.jpg?sign=1739282062-aHBSJ3a940Ung1n9khtmpbGPV298U1R0-0-e4cdfce43d424ec20d37ad7d7a338e8b)
将会提示语法错误。
6.工厂方法的构造方法
前面所述的众多构造方法均会返回一个新的实例。为了减少硬件资源的消耗,Dart的设计者提供了工厂方法。所谓工厂方法,就是提供缓存,如果一个对象已经被实例化过,那么从缓存中取出来返回即可,不需要再生成一个新的对象。
因此,使用工厂方法不一定总是返回一个新的对象。也正因为如此,它可以减少实例化对象的时间。参考下面名为Person_2的新类:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_41.jpg?sign=1739282062-UppenO5SCPIiugGV69Tmw3hlewrNSTjH-0-5abb06611ef98e1ea41914cf4c4e8383)
在创建缓存区时使用了Map的组织形式,在类的内部使用了_cache对象。前面的String即key,用于保存某个人的姓名。只要有了姓名,就能找到他。然后,回到main()方法中写下如下代码:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_42.jpg?sign=1739282062-nHWSO32zCznCMAsoPnboAIjVF1gN1c2y-0-b8db4579e827f3e9cd3eb22acf31d064)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_43.jpg?sign=1739282062-BNc8RylQj7MEEKSZXBbU3RWwha372oaW-0-691d7bfb7f5ebe83ef4d742e3d82666b)
通过调用对象.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=1739282062-6UueZi8qvWM0Sq2QFqXgq1OrvX1aWcb4-0-bdca0cce38c167857fec3a32229b82de)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_45.jpg?sign=1739282062-mGXrZaT8q30ERmg7FSTaMVxqhe9OEDOb-0-98d38372b47e1670aebb310304194b13)
注意,在最后的readme()方法前加了static关键字,这是一个静态方法。在使用静态方法时,只需要使用“类名.静态方法”即可。具体如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_46.jpg?sign=1739282062-FExhqFNrOCDq0t9mukKy747u7w9YgRcN-0-d2ce46264a9bbfba54e8f81dc62c17f8)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_47.jpg?sign=1739282062-adMMfEXjMriteBoF7Cvo7WktJUJ5NOHT-0-5d1b407dd1c0474aa8deb4df7cae9923)
同样地,如果使用类的实例,如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=1739282062-fVz14iRfwYytt7xLddxr3hPzgAhGuA9l-0-1aa8d0dc8049da95b1bfddcfda6ebc3f)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_49.jpg?sign=1739282062-DcU7KsqYIPAzJHtcwmzVhV0OXCsXS7OO-0-421488ba9f643631f5edfdefadc632d7)
可以看到,父类的内容很简单:三个实例变量分别对应屏幕尺寸、手机质量和发布时间;打电话和发短信两个实例方法分别需要被叫号码、接收号码和短信内容。下面继续新建用来表示iPhone手机和Android手机的子类。
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_50.jpg?sign=1739282062-sHnvfD0sUMyuMM57P0EoduiSpAQZCww7-0-e1a7fbe0d337552e41cbb1fe5de6af46)
再回到main()方法中,分别实例化这两部不同的手机:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_51.jpg?sign=1739282062-Nnk4l6tpOCm2DBqiDuKmTQU4buOnOGfl-0-c9939f366401d59ee51918520d97c85e)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_52.jpg?sign=1739282062-3COxNl63WmKtcrYLXwt5LJZ2rDKN6FXY-0-ff6b31bfd08f67966c5c50f1036d5eb4)
最后调用各自的toString()方法、call()方法和sendSms()方法,结果如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_53.jpg?sign=1739282062-ZBMo9DtQ7QcQzoLJFbNuiVVoslttT1OM-0-1433c1e9a20bf68362c9b64a209cd81f)
可见,虽然这两个类都继承了MobilePhone,但是又有各自的特色。对于MobilePhone中的变量和方法,就不需要再重复编码了。当然,你也可以自由地输出其他信息。
对于上例,如果我们不满足于单纯调用父类的方法,而是想在各自的方法中加上一个手机品牌信息,就要借助super关键字来复写父类的方法。具体的操作如下:
对于iPhone手机:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_54.jpg?sign=1739282062-5dcqCJxmkFW0avP1znn9t4ynShPpySlq-0-726012576abc903e4941b8d43de771e4)
Android手机:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_55.jpg?sign=1739282062-sGXVXVXdGm0dPTCgoQvhuzeIPb89yo3n-0-f50b082aeda5e903059427e7b07ff5f9)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_56.jpg?sign=1739282062-BhmRlbRE7sVXA0uon3dcDjYr2saoMplO-0-2a98df1ff1977e5639d161e726909dc0)
main()方法中的内容保持不变,运行结果如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_57.jpg?sign=1739282062-kqiBr7TQ1x38NK4Csjr287BbT2bMxBMo-0-ac60d3be5c00b8dcab52632afca9c2e8)
如果不希望父类的方法被调用,就去掉super这一行。
4.1.9 可复写的运算符
运算符也可以被复写,但是并非所有的运算符都能复写。能够复写的运算符如表4.1所示。
表4.1 可复写的运算符
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_58.jpg?sign=1739282062-q7lnTp9Esac9ion1ZhO7MBNdcKJe5Icp-0-8bd6017424feba03c5f3d8656af0b7cf)
定义一个用于两项整数分别相乘的类,在其中复写乘号(*)运算符:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_59.jpg?sign=1739282062-eocqvfhFecWokeBcaImzvyPHnYo3Krbh-0-a7457b3cb4b8d1eb9eeb59069ddd9704)
在main()方法中实例化两个对象,并让它们相乘:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_60.jpg?sign=1739282062-Den74mGPKqCyy0qSfvGU2F4x7UMmw7Ve-0-1933d4ec5aadf85cfb190c5df3425610)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_61.jpg?sign=1739282062-QexB9lwdvjKjZ8jIAhnQLt4LFhBwphTJ-0-9156b40c9bbded5b15a59f1e3f4f92e5)
可见,代码按照我们希望的逻辑运行,实现了两个整数分别相乘的需求。当然,你也可以尝试两个数交叉相乘,或者自定义其他运算符的功能。
4.1.10 抽象方法
在Dart中,支持抽象方法。它和实例方法不同,实例方法要求完整的方法名和方法体,即方法的实现;但是抽象方法则只要求方法名,方法体在其子类中实现。回到之前讲解继承的例子中,尝试将父类声明为抽象类,并将其中的打电话(call())方法改为一个抽象方法。完整的代码如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_62.jpg?sign=1739282062-hKsUw4IdcpoMltQ1llys1IZxfyUpiFOL-0-829394f358ac1383c7d190e83f9e7291)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_63.jpg?sign=1739282062-FcL4AJExdF8mIpPfaQhxOmnxvpwYLgtO-0-c465c94ad9d36d0d35561a923c3ac3be)
此时,两个子类均会在原有的super.call()方法上报语法错误,改正如下:
iPhone类:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_64.jpg?sign=1739282062-of9TCy93I6VhZkjLAffYcC8hPTW5uiSr-0-4942773d9f5f5ab679b90c07f93a66ba)
AndroidPhone类:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_65.jpg?sign=1739282062-vW4YyXlA546SpiS7UF1ZrFy5ABnumhqz-0-4b2927ff0154810ae7fdc1020c4e1103)
main()方法中的内容保持不变,运行结果如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_66.jpg?sign=1739282062-uXNt7hRiKA1jyURHNC74ZFSUXjoLBRZJ-0-65d625fe0eb75088c8e6a0fa7c232816)
综上所述,抽象方法就是仅对方法进行声明,然后放到子类中具体实现。
4.1.11 抽象类
上例中,我们将名为MobilePhone的父类声明为抽象类,这样MobilePhone便成了一个抽象类。抽象类无法被实例化,这也就意味着无法通过new MobilePhone()方法来初始化一个对象。
抽象类一般会包含一个或多个抽象方法,同时也允许具体的实例方法存在。由于在上例中使用过抽象类,因此这里就不再重复举例。
4.1.12 接口
对于之前的例子,现在需要实现一个相同的功能,就是报出手机的品牌,但是具体的实现方法在另外一个类中。
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_67.jpg?sign=1739282062-yjMsTxYynEYM7XKODinm6L1utQ1hcQEJ-0-197ee140adcda8e8891f4274787f29e5)
由于Dart编程语言是不支持多个类继承的,因此在这种情况下,就要用到接口的概念。实现接口的关键字是 implememnts,我们采用和继承类似的写法改造iPhone类和AndroidPhone类。
iPhone类:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_68.jpg?sign=1739282062-L4tu6PBq1XaPFYkjhDAjchblBT5TUWVq-0-3d8f9024657ccb6a4db33476e719b724)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_69.jpg?sign=1739282062-MmJyAIfrkunK50HreMRMFvx5mSESdXgo-0-0c96ed210ab076498e9384a490cdec94)
AndroidPhone类:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_70.jpg?sign=1739282062-f1F3MUMRLQHJDZPgyGsvhH5R0CvHfTg2-0-ebef07d6587c6ee4280ee34ed88c2399)
最后,在main()方法中分别调用两个类实例的printMyBrand()方法,得到结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_71.jpg?sign=1739282062-AF2s2fVtiQgf6oQrxsJkGKouErUrpthD-0-68742797b768e5b8a2c261d0488d8568)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_72.jpg?sign=1739282062-70AXsf4zoOgiMKXdICmgSkWzy43CMsBg-0-799d76bebf87f84ab9e4622b22ef0104)
上例中的玄机在于使用了implements。有了它,便有了方法实现的多样性,这对于上例中的应用场景十分合适。而且,在接口的实现上,Dart编程语言是支持多实现的。其结构如下:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_73.jpg?sign=1739282062-uUv8bLDac3mXnUOSSURuOrmeLDfE4gKG-0-d7b8baa480646161708f992748ecb13a)
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=1739282062-f0TwxIzAs2a84uNSFY5QIIb9TWZetJFp-0-bd57b75dc749cc88c69c431817595986)
由于这次不需要再重新实现它,因此使用implements就显得不太合适,这时就需要Mixin特性来救场。要使用Mixin特性就需要用到with关键字,后面紧跟着类名。特别注意的是,要将它们放在extends之后,implements之前。下面来看一下修改后的AndroidPhone类:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_75.jpg?sign=1739282062-kZqaNaXWG7x7YeGopdbEXl65GKrKg8dp-0-f6296b77ff2bee79318b3b63664190fc)
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_76.jpg?sign=1739282062-HinGviym0VjvndmJ3YMyYDC6B8EVtq65-0-e68b0fe2887fd3c8fa563ceba89b84bd)
该类的首行使用了with关键字,而且在本类中并没有重写BackgroundApp()方法。接下来,在main()方法中调用:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_77.jpg?sign=1739282062-EfHEpMxSczGEAAGyuk0HhxdtH8qUONa4-0-92159bb813d2f6708b0f0f4c4fa6960e)
运行结果:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_78.jpg?sign=1739282062-xKKsTdefxWjIt0IBD18q9OfF7CmkVR5I-0-aa383726f063ddfd5996811b2dfff05b)
可见,方法已经被添加到AndroidPhone类中并成功调用了。当然,作为with后的方法也是可以复写的,复写的原则和继承后的复写类似。
4.1.14 枚举
枚举,即enums或enumerations,是一种特殊的类。它通常用来表示具有固定数目的常量,但是它无法继承,无法使用Mixin特性,也无法实例化。先来看一个示例:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_79.jpg?sign=1739282062-TTaRlAMc1oJEDm83382NBWKTfVAj6zXv-0-08b9c708d5b8d39c6496567f756f7aa8)
这是一个典型的枚举示例,用来表示Android设备的品牌。类似于列表,它的下标也是从0开始的,使用index可得到下标值:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_80.jpg?sign=1739282062-tHolEaZyLKi50lReClyreIhvh0Qx5H8x-0-0932b769ebcaaca1750a8e4b110e4000)
在实际开发中,枚举可以帮助我们写出更易懂的代码。比如,要判断一款手机的品牌,仅需如下操作:
![img](https://epubservercos.yuewen.com/9CF474/18096059801207706/epubprivate/OEBPS/Images/txt004_81.jpg?sign=1739282062-ksA9fOqDzwLVdInr15iKPCEcbq8rN7qW-0-d4e8d0aabaabe9bd1ad0e67540fab925)
这样可减少由于拼写失误导致的异常,另外,当需要发生变化时,仅需对枚举类的内容进行修改即可,不需要在调用它的代码位置修改,降低了维护成本。