![用Flutter极速构建原生应用](https://wfqqreader-1252317822.image.myqcloud.com/cover/311/27563311/b_27563311.jpg)
3.2 Dart中的类
Dart是一门基于类和继承的面向对象语言,讲到对象,我们就不得不谈类。对象是类的实例,对象也是由类构造出来的,更通俗地讲,我们可以把类理解为对象的模板,在类中定义了对象的属性和方法。
3.2.1 自定义类与构造方法
我们之前在Dart中使用的任何数据类型其实都是类,包括描述整数的int类、描述浮点数的double类、描述字符串的string类等。在开发中,我们可以根据实际需要定义自己的类,在Dart中,使用class关键字进行类的定义,示例如下:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P61_81368.jpg?sign=1739284586-ucTjmZsAURDWtxovcQR135nXuEgUVhZ6-0-1eb29ec8d04c0d7c979d10bd7c5a730f)
上面我们简单定义了一个圆形类,其中定义了3个属性,分别描述圆的半径和圆心的X、Y坐标。尽管我们没有给这个圆形类定义任何方法,但它已经是一个完整的自定义类了,我们可以通过它构造圆形对象来存储数据。
通过类创建对象需要调用类的构造方法,类的构造方法通常与类名一致,也可以定义独立名称的构造方法,但是依然需要通过类名来调用。当我们定义完一个类时,Dart默认会生成一个没有参数的构造方法,我们可以直接通过类名进行调用,例如:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P61_81369.jpg?sign=1739284586-Ybp3SEwgvjLZTGWCyLkvy4ke2jgrcJtd-0-6f4f68569c954239d2e2f027f09b2603)
类实例中封装的属性可以通过点语法来进行访问,可以进行属性值的设置,也可以进行属性值的读取。在默认情况下,如果没有对实例的某个属性进行过赋值,此属性的值就为null。但是,在进行类的定义时,也可以为属性提供默认值,例如:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P62_81370.jpg?sign=1739284586-i2s1lA2mfkxvRx5zLAwGM8T5ux7oQL4j-0-632bc583d2169f0bce102429c39c84a0)
构造方法是类的实例对象生成过程中的重中之重。Dart默认提供的构造方法虽然可以完整地构造出对象,但是其中定义的属性都为null值或默认值。我们也可以通过实现构造方法来干预对象生成的过程,例如:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P62_81371.jpg?sign=1739284586-r4HztzU5k9YsfABsrkK5c2TDNWFJ5wxw-0-2451a68d2f3e3608de4f393c96ad9118)
如上所示,重写的构造方法中定义了3个参数,分别对应圆形的半径和圆心点X、Y坐标。在使用Circle类构造对象时,需要将指定的参数传入,例如:
var circle = new Circle(6,1,1);//使用参数构造圆形对象
有一点需要额外注意,一旦重写了构造方法,默认的无参构造方法将不再可用。在上面的构造方法中,this关键字指的就是当前实例对象,构造方法的实质是将对象属性的赋值过程由外界封装到类的内部。
在Dart中,关于构造方法的编写还有一个小技巧,一般情况下,我们可以直接将构造方法定义成如下模样,Dart会自动进行参数和属性的匹配,进行赋值,非常方便。
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P62_81372.jpg?sign=1739284586-dCeksK9XLyXgmnjthyLKD7Ek91RHW2M4-0-c41fb95c2706af7bd1aa05a4dbc80824)
有时,一个类需要有多个构造方法,比如自定义的圆形类,很多时候需要快速创建出单位圆(圆心为坐标原点、半径为1的圆)。这时,就可以定义一个便捷的构造方法帮助我们直接生成单位圆,这类构造方法也被称为命名构造方法,示例如下:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P63_81375.jpg?sign=1739284586-3f9qH0HrdrA1GTN5xHTprZG1u8xnOVnn-0-78da7c98e10cdfd87275351a7b757de3)
命名构造方法通常用来快速地创建标准对象,同样,命名构造方法也可以有参数,并且只要参数名与类中定义的属性名一致,也可以使用Dart自动匹配赋值的特性。
在Dart中,类还有一个强大的功能是支持继承,关于继承的内容后面会详细介绍,但是这里你需要牢记,构造方法不会被继承。
3.2.2 实例方法
类封装了属性和方法,属性用来存储描述类的数据,方法用来描述类的行为。在面向对象编程中,生活中的事物都可以模拟成程序中的对象,例如一个教务系统软件中一定有教师相关信息,每一位教师都是一个教师对象,可以创建教师类来描述教师对象,示例代码如下:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P63_81376.jpg?sign=1739284586-tXZvOzbnL5wVMfh7vNfa4CDZgA96Vly3-0-8107b48e3c06d15fa97363590f34ee28)
上面的代码为教师类添加了3个属性,name属性用来描述教师的名字,number属性用来描述教师的编号,subject属性用来描述教师教学的课程。除了属性外,还为教师对象添加了sayHi方法与teaching方法,方法的用法和函数一样,只是在调用时需要用对象来调用,并且方法中会自动将当前对象绑定到this关键字上。也就是说,在方法中可以通过this关键字获取对象的属性信息,也可以调用其他方法。方法也需要通过点语法来进行调用,例如:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P64_81378.jpg?sign=1739284586-G8YpDbzsWLON23VHK7waOTMohBB3FvoB-0-31623590deeb370d0076f4a9fa7bf39d)
类中还有两个非常特殊的方法:Setters方法与Getters方法。Setters方法用来设置对象属性,Getters方法用来获取对象属性。其实当我们使用点语法访问对象属性信息时,调用的就是Setters方法或Getters方法,在定义属性时,Dart会自动生成默认的Setters方法和Getters方法。Setters方法和Getters方法的另一大作用是定义附加属性,附加属性也可以理解为计算属性,即这些数据通常不是描述对象的最原始数据,而是通过计算得来的,例如:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P64_81379.jpg?sign=1739284586-uhNfYwmh8WHdzbdyCLbXOrHwHkwYv8IX-0-70815c5037888954c88640518fbf8ed2)
上面的代码中,description就是附加属性,其并没有真正占用内存空间进行存储,而是通过其他属性计算而来的。
3.2.3 抽象类与抽象方法
抽象类是面向对象开发中较为难理解的一点。在Dart中,抽象类中可以定义抽象方法。所谓抽象方法,是指只有定义却没有实现的方法,抽象是面向接口开发的基础。以生活中汽车产品的生产为例,一辆完整的汽车的生产往往需要多个厂家合作,例如发动机生产厂家、轮胎生产厂家、门窗内设生产厂家等。不同的厂家生产的配件若要完美地组合成一辆汽车,则必须遵守统一的标准,也可以理解为按照实现的协议进行生产。在编程中也是这样的,一个复杂的程序可能需要很多开发者甚至多个部门进行配合开发,每个开发者或部门负责一个模块,而模块之间又可以进行交互与连通,这时在程序真正编写前,我们就需要先约定协议、制定接口。
现在你应该理解了,抽象类实际上就是一个接口,接口中定义了未实现的方法告诉调用者:如果有类实现了这个接口,这个类就拥有接口所描述的功能。例如,我们可以为教师类定义一个接口,示例如下:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P65_81381.jpg?sign=1739284586-LnNgNHpSNPLEJCP7b50PMAnU248gsI1W-0-fbb2ee780ac2619ab896378afae106ae)
上面的TeacherInterface接口中只定义了一个抽象方法,Teacher类可以对这个接口进行实现,示例代码如下:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P65_81382.jpg?sign=1739284586-BMJj4UYLEqoZGtAATXIwawYBZH4f8a97-0-ff816011d19286b62194b349dcc8fac6)
一个类也可以同时实现多个接口,例如再定义一个人类接口,示例如下:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P65_81383.jpg?sign=1739284586-DNPSbD7Y1ukHA61n4NS73iFlZvVGTh63-0-1145398407d532377736cf14aae397cc)
抽象类不可以被实例化,即不能直接使用抽象类来构造实例对象,只能通过实现这个抽象类接口的类或者继承它的子类来实例化对象。关于继承的内容,后面会介绍。
3.2.4 类的继承
继承是类的重要特性。子类继承父类后,可以直接使用父类中定义的属性和方法,并且子类可以对父类的方法进行重写以实现定制化的功能。继承其实很容易理解,现实中的事物为了方便描述与归纳,也会进行分门别类,例如生物界可以分为动物和植物,动物类下面又可以分出鱼类、鸟类等,动物类就是生物的子类,鱼类、鸟类又是动物类的子类。越是上层的类,封装的属性和方法越通用,子类会在父类的基础上进行扩展,增加许多独特的属性和方法。
在Dart中,使用extends关键字进行类的继承。以教师类为例,我们可以定义一个人类作为其父类,示例如下:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P66_81385.jpg?sign=1739284586-lcVpKshKXK5QzzgYEuccbLWP6aOy26UI-0-93b155185c36d340b22821d316e843c3)
如上面的代码所示,Teacher类直接继承了People类的姓名、年龄属性和sayHi方法。但是需要注意,构造方法是不会被继承的,在Teacher类中可以使用super关键字来调用父类的方法,包括构造方法。子类也可以重载父类的方法,并且在重载时可以调用对应的父类方法,例如:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P67_81387.jpg?sign=1739284586-zrj91pYdTZIv16C64v8p7UyK827LMEdw-0-519f5892f862457ba269054b998085e7)
其中,@override关键字可以省略,这个关键字的作用仅仅是标注这个方法是子类重载父类的。
3.2.5 运算符重载
我们前面在学习运算符相关内容时了解到,Dart中的运算符非常灵活,例如加法运算符除了可以用在数值的加法运算外,在字符串对象间使用也可以实现字符串的拼接功能。Dart中的运算符之所以如此灵活,是由于Dart是一门完全面向对象的语言,而运算符的运算实质是方法的调用。因此,我们也可以为自定义的类添加运算符方法,例如:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P67_81388.jpg?sign=1739284586-eD3luUFwNuSKvesQs4XL1iQ80I8yLgHc-0-a88fd692fe4aab63812b19cc815ce5e7)
上面的代码定义了一个尺寸类Size,类中定义了宽度与高度两个属性,operator关键字用来进行运算符的重载,其格式如下:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P68_81389.jpg?sign=1739284586-nNDlUVG9or0p1mKX9vT0AXu16hNIXoCp-0-fc998aea11a40f2b914c06d92124b1b3)
上面的代码重载了Size类的加法运算,当将两个Size对象进行相加时,分别将它们的宽度和高度进行相加,并将新的对象返回。
重载运算符非常简单,却是非常强大的一个功能,在Dart中支持重载的运算符如表3-1所示。
表3-1 支持重载的运算符
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-T68_81395.jpg?sign=1739284586-sSaRG2D6PbAUx8UyzRKuqSWuxwRMLSQL-0-5730777cf4bce7b6f4185467818d0c73)
3.2.6 noSuchMethod方法
对于非抽象类,当定义了一个没有实现的方法时,代码的运行会产生异常,例如:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P68_81392.jpg?sign=1739284586-FalHsoq8xqtCxsmlvy7u0uzF84mepizF-0-8f316fad05e2e517f61f1bbfd55e1908)
运行上面的代码,控制台会抛出如下的异常信息:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P68_81393.jpg?sign=1739284586-0aBuenRMphyRe9zxXkwRfHRkVokyVp9c-0-b7d3ac4309898d2653e521648651f9e6)
其实,如果一个类实现了某个接口或者继承了某个抽象类,却没有全部实现接口和抽象类中声明的方法,就会产生如上的异常。然而实际开发中,接口或抽象类中的方法有时并不需要全部实现,这时可以选择重载noSuchMethod方法,如果重载了这个方法,当对象调用到这些未实现的方法时,就会执行noSuchMethod方法,例如:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P68_81394.jpg?sign=1739284586-PPvjWechHExIvKXMU4a2WR2M5afXiTTO-0-f439aebfde76d23cc80c9217287749e0)
需要注意,虽然重载noSuchMethod方法可以避免调用未定义方法异常的产生,但是其也会掩盖代码逻辑中的错误,在实际开发时,要尽量少使用这种方式。
3.2.7 枚举类型
枚举是一种特殊的类型,其用来描述有限个数的数据集合。比如前面在定义教师类时,其中定义了一个科目的属性,虽然我们将其定义为字符串类型,但是这并不十分严谨,教师所教学科目的类目是有限的,而且应该是固定的,不会随意增减,对于这种情况,使用枚举非常合适。
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P69_81397.jpg?sign=1739284586-v4zEIzdWHqTL9IywDeRtShG0scBvNbMp-0-3b16f62aa3025a62e958f463c9455e6f)
enum关键字用来定于枚举,其实枚举与数组类似,其中的数据也都有下标,从0开始,例如:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P69_81398.jpg?sign=1739284586-bjT33VYp6Ly4BvBmDNB6HhXpLyWMpMCL-0-c3fcf7dfd5461b57da5a319aa21f32e3)
也可以使用values属性来获取枚举中所有的值,例如:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P70_81402.jpg?sign=1739284586-VwoAh4i9m57jAMi0RIvMIKVaTwqmQ5YR-0-d4e7b0b3f456fe2875d8f689f26f829d)
更多时候,枚举会和多分支语句结合使用,示例代码如下:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P70_81403.jpg?sign=1739284586-YzUyPaIqFdMvYa1klyixTvztr9Odr0lx-0-7bf2b07d6729dd042c9be474fe784acd)
3.2.8 扩展类的功能——Mixin特性
Mixin是Dart中非常强大的一个特性。通过前面的学习,我们知道,Dart只支持单继承,即子类只能够有一个父类。有的时候,我们需要集合多个类的功能来实现一个复杂的自定义类,就需要使用到Mixin特性。
Mixin从字面来理解为混合的意思,顾名思义,Mixin的主要作用也是进行混合,其允许一个类将其他类的功能混合进来,例如:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P70_81404.jpg?sign=1739284586-fLZzyZDJAuzS7qHUAFigkXh3I0cz2mhd-0-c91dc0d5f7dd5a8115f3414ef3833d9d)
从控制台的打印信息可以看出,Bird类已经成功混合了Descript类中的功能,可以调用其中定义的方法。
能够进行混合的类被称为Mixin类,Mixin类中不能实现构造方法,否则不能够被其他类进行混合。使用with关键字来进行Mixin混合,Mixin支持多混合,例如:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P71_81406.jpg?sign=1739284586-VsjACnCnC6hYcSH0ksLFNU4dd1sE23Rf-0-828a2038e14b6ffd78dba47317187393)
还有一点需要额外注意,作为Mixin的类虽然不能够定义构造方法,但是可以使用默认的构造方法进行实例化,如果不想使Mixin类实例化,那么可以使用mixin关键字代替class关键字来定义Mixin类,示例如下:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P72_81407.jpg?sign=1739284586-nzkQQvjA3wuGPMKKJWLOcGUgY3k9GjMV-0-f0eca5fd0c8db90ca3180341a9621821)
使用mixin定义的Mixin类不能够被继承,也不能够进行实例化。Mixin类本身可以进行继承,如果使用class关键字进行定义,就和普通类的集成语法一致,如果使用mixin关键字进行定义,就使用on关键字进行继承,例如:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P72_81408.jpg?sign=1739284586-X90z0Pb4RnPEpiBmnlwASRkUTzdXQ7A9-0-70bc9c7dba693f09f0cd5d3934d39452)
提示
mixin关键字在Dart 2.1版本之后可用。
学习了Mixin的基本概念与简单用法,下面我们需要更深入地了解Mixin的工作原理,首先观察下面的示例:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P72_81409.jpg?sign=1739284586-Ecj2aG8zSFZt26hCDQaNPiLMO4nNKcq3-0-ab70de2b62e85675b427ca567550c490)
上面的代码使用到了继承和多混合,并且子类、父类、Mixin类中都对相同的方法进行了实现,运行代码,控制台会打印出“sub func”。可以看出,无论是Mixin还是继承,子类中的方法实现优先级都是最高的,将子类实现的方法去掉,再次运行,控制台将输出“two func”,这说明Mixin中方法的优先级要高于父类中方法的优先级,并且在多混合中,后混合的优先级更高。
因此,在对于继承和混合一起使用的复杂场景中,你需要牢记如下两个原则:
(1)当前类中方法的优先级最高。
(2)Mixin中方法的优先级高于继承父类方法的优先级,并且在混合时,Mixin从左到右优先级依次增高。
3.2.9 类属性与类方法
前面我们定义类时定义的属性和方法都是针对实例的,即由类的实例对象进行访问或调用。其实,类本身也是一种对象,在类中也可以定义类属性与类方法,使用类名直接进行访问和调用。示例代码如下:
![](https://epubservercos.yuewen.com/4C63B7/15825992104143906/epubprivate/OEBPS/Images/Figure-P73_81411.jpg?sign=1739284586-DQst1D6rayrYuNFwzPY6GY8tCcow02ZX-0-6c9f4e514d1f8a4c8d4d710488200215)
类属性也被称为类静态属性,其通常用来存放某些固定的且在类的所有实例中共享的属性,类方法也被称为类静态方法,通常会提供一些静态的计算功能。