面向对象篇
Swift语言中的面向对象特性
在现代计算机语言中,面向对象是非常重要的特性,Swift语言也提供了面向对象的支持。而且在Swift语言中,不仅类具有面向对象特性,结构体和枚举也都具有面向对象特性。
面向对象概念和基本特征
面向对象(OOP)是现代流行的程序设计方法,是一种主流的程序设计规范。其基本思想是使用对象、类、继承、封装、属性、方法等基本概念来进行程序设计。从现实世界中客观存在的事物出发来构造软件系统,并且在系统构造中尽可能运用人类的自然思维方式。
**OOP的基本特征包括:封装性、继承性和多态性。
封装性 封装性就是尽可能隐蔽对象的内部细节,对外形成一个边界,只保留有限的对外接口使之与外部发生联系。
继承性 一些特殊类能够具有一般类的全部属性和方法,这称做特殊类对一般类的继承。通常我们称一般类为父类(或基类),特殊类为子类(或派生类)。
多态性 对象的多态性是指在父类中定义的属性或方法被子类继承之后,可以使同一个属性或方法在父类及其各个子类中具有不同的含义,这称为多态性。例如动物都有吃饭的方法,但是老鼠的吃饭方法和猫的吃饭方法是截然不同的。
Swift中的面向对象类型
面向对象,在不同的计算机语言中,其具体的体现也是不同的。在C++和Java等语言中通过类实现面向对象,在Swift语言中通过类和结构体(struct)实现面向对象,在Swift语言中,枚举(enum)也具有面向对象特性。结构体和枚举在其他语言中完全没有面向对象特性,Swift语言赋予了它们面向对象生命。
提示:由于OOP中的类在Swift语言中涵盖了枚举、类和结构体。为了防止与OOP中的类发生冲突,在此把Swift中的这3种类型称为“Swift面向对象类型”。
在面向对象中,将类创建对象的过程称为实例化,因此将对象称为实例,但是在Swift中,结构体和枚举的实例不称为“对象”,因为结构体和枚举并不是彻底的面向对象类型,而是包含了一些面向对象的特点。例如,在Swift中继承只发生在类上,结构体和枚举不能继承。
在Swift中,面向对象的概念还有:属性、方法、扩展和协议等,这些概念对于枚举、类和结构体等不同类型有可能不同。
枚举
在C和Objective-C中,枚举用于管理一组相关常量集合,通过使用枚举可以提高程序的可读性,使代码更清晰,更易于维护。而在Swift中,枚举的作用已经不仅仅是定义一组常量、提高程序的可读性了,它还具有了面向对象特性。
枚举的语法格式:
enum 枚举名
{
枚举的定义
}
"枚举名"是该枚举类型的名称。它首先应该是有效的标识符,其次应该是遵守面向对象的命名规范。它应该是一个名称,如果采用英文单词命名,首字母应该大写,尽量用一个英文单词。这个命名规范也适用于类和结构体的命名。“枚举的定义”是枚举的核心,它由一组成员值和一组相关值组成。
成员值
在枚举类型中定义一组成员,与C和Objective-C中枚举的主要作用是一样的,不同的是,在C和Objective-C中成员值是整数类型,因此在C和Objective-C中枚举类型就是整数类型。
而在Swift中,枚举的成员值默认情况下不是整数类型。
声明枚举示例:
print("--枚举示例---")
enum WeekDays {
case Monday
case Tuesday
case Wednesday
case Thursday
case Friday
}
上述代码声明了WeekDays枚举,表示一周中的每个工作日,其中定义了5个成员值:Monday、Tuesday、Wdenesday、Thursday、Friday,这些成员值并不是整数类型。
在这些成员值前面还要加上case关键字,也可以将多个成员值放在同一行,用逗号隔开,如下:
enum WeekDays_ {
case Monday, Tuesday, Wednesday, Thursday, Friday
}
看一个示例,代码如下:
print("---使用枚举示例----")
var day = WeekDays.Friday
day = WeekDays.Wednesday
day = .Monday
func writeGreeting(day : WeekDays) {
switch day {
case .Monday:
print("星期一好!")
case .Tuesday:
print("星期二好!")
case .Wednesday:
print("星期三好!")
case .Thursday:
print("星期四好!")
case .Friday:
print("星期五好!")
}
}
writeGreeting(day)
writeGreeting(WeekDays.Friday)
前三行代码均是给变量day赋值,可以采用完整的“枚举类型名.成员值”的形式,也可以省略枚举类型,采用“.成员值”的形式。这种省略形式能够访问的前提是,Swift能够根据上下文环境推断类型。
枚举类型与switch语句能够很好地配合使用,在switch语句中使用枚举类型可以没有default分支,这在使用其他类型时是不允许的。
需要注意,在switch中使用枚举类型时,switch语句中的case必须全面包含枚举中的所有成员,不能多也不能少,包括使用default的情况下,default也表示某个枚举成员。
使用default分支的代码:
print("---使用default分支---")
func writeGreeting_(day :WeekDays) {
switch day {
case .Monday:
print("星期一好!")
case .Tuesday:
print("星期二好!")
case .Wednesday:
print("星期三好!")
case .Thursday:
print("星期四好!")
default:
print("星期五好!")
}
}
在上面示例中,default表示的是Friday枚举成员,在这种情况下,Friday枚举成员的case分支不能再出现了。
原始值
为每个成员提供某种具体类型的默认值,可以为枚举类型提供原始值(raw values)声明,这些原始值类型可以是:字符、字符串、整数和浮点数等。
原始值枚举的语法格式如下:
enum 枚举名 : 数据类型
{
case 成员名 = 默认值
...
}
在“枚举名”后面跟“:”和“数据类型”就可以声明原始值枚举的类型,然后在定义case成员的时候需要提供默认值。
以下代码是声明枚举示例:
print("---原始值---")
enum WeekDays__ : Int {
case Monday = 0
case Tuesday = 1
case Wednesday = 2
case Thursday = 3
case Friday = 4
}
声明的WeekDays枚举类型的原始值类型是Int,需要给每个成员赋值,只要是Int类型都可以,但是每个分支不能重复。还可以采用如下简便写法,只需要给第一个成员赋值即可,后面的成员值会依次加1。
print("--原始值--简便写法--")
enum WeekDays___ : Int {
case Monday = 0, Tuesday, Wednesday, Thursday, Friday
}
完整示例代码:
print("---完整示例代码----")
print("---原始值---")
enum WeekDays : Int {
case Monday = 0
case Tuesday = 1
case Wednesday = 2
case Thursday = 3
case Friday = 4
}
var day = WeekDays.Friday
day = WeekDays.Wednesday
func writeGreeting(day : WeekDays) {
switch day {
case .Monday:
print("星期一好!")
case .Tuesday:
print("星期二好!")
case .Wednesday:
print("星期三好!")
case .Thursday:
print("星期四好!")
case .Friday:
print("星期五好!")
}
}
let friday = WeekDays.Friday.rawValue
let thursday = WeekDays(rawValue: 3)
if (WeekDays.Friday.rawValue == 4) {
print("今天是星期五")
}
writeGreeting(day)
writeGreeting(WeekDays.Friday)
let friday = WeekDays.Friday.rawValue该句代码是通过WeekDays.Friday的方法rawValue转换为原始值。虽然在定义的时候Friday被赋值为4,但是并不等于WeekDays.Friday就是整数4了,而是它的原始值为整数4,因此下面的比较是错误的。
if (WeekDays.Friday == 4) {
print("今天是星期五")
}
.rawValue方法是将成员值转换为原始值,相反,WeekDays(rewValue : 3)方法是将原始值转换为成员值。
相关值
在Swift中除了定义一组成员值,还可以定义一组相关值(associated values),它有点类似于C中的联合类型。
枚举类型的声明:
print("--相关值---")
enum Figure {
case Rectangle(Int, Int)
case Circle(Int)
}
枚举类型Figure(图形)有两个相关值:Rectangle(矩形)和Circle(圆形)。Rectangle和Circle是与Figure有关联的相关值,它们都是元组类型,对于一个特定的Figure实例,只能是其中一个相关值。
示例,代码如下:
func printFigure(figure : Figure) {
switch figure {
case .Rectangle(let width, let height):
print("矩形的宽:\(width) 高:\(height)")
case .Circle(let radius) :
print("圆形的半径:\(radius)")
}
}
var figure = Figure.Rectangle(1024, 768)
printFigure(figure)
figure = .Circle(600)
printFigure(figure)
如果某个相关值元组中字段类型一致,需要全部提取,则可以在相关值前面添加let或var。可以使用如下方式修改Rectangle分支:
switch figure {
case let .Rectangle(width, height):
print("矩形的宽:\(width) 高: \(height)")
case .Circle(let radius):
print("圆形的半径:\(radius)")
}
结构体与类
在面向过程的编程语言中,结构体用的比较多,但是面向对象之后,如在C++和Objective-C中,结构体已经很少使用了。这是应为结构体能够做的事情,类完全可以取而代之。
而Swift语言却非常重视结构体,把结构体作为实现面向对象的重要手段。Swift中的结构体与C++和Objective-C中的结构体有很大的差别,C++和Objective-C中的结构体只能定义一组相关的成员变量,而Swift中的结构体不仅可以定义成员变量(属性),还可以定义成员方法。因此,可以把结构体看做是一种轻量级的类。
Swift中的类和结构体非常类似,都具有定义和使用属性、方法、下标和构造器等面向对象特性,但是结构体不具有继承性,也不具备运行时强制类型转换、使用析构器和使用引用计等能力。
类和结构体定义
Swift中的类和结构体定义的语法也是非常相似的。我们可以使用class关键词定义类,使用struct关键词定义结构体,它们的语法格式如下:
class 类名 {
定义类的成员
}
struct 结构体名 {
定义结构体的成员
}
从语法格式上看,Swift中的类和结构体的定义更类似于Java语法,不需要像C++和Objective-C那样把接口部分和实现部分放到不同的文件中。
示例:
print("---类和结构体定义---")
class Employee { //定义员工类
var no : Int = 0 //定义员工编号属性
var name : String = "" //定义员工姓名属性
var job : String? //定义工作属性
var salary : Double = 0 //定义薪资属性
var dept : Department? //定义所在部门属性
}
struct Department { //定义部门结构体
var no : Int = 0 //定义部门编号属性
var name : String = "" //定义部门名称属性
}
Employee是定义的类,Department是定义的结构体。
可以通过下列语句实例化:
var emp = Employee()
var dept = Department()
Employee()和Department()是调用它们的构造器实现实例化。
提示:实例化之后会开辟内存空间,emp和dept被称为“实例”,但只有类实例化的“实例”才能被称为“对象”。事实上,不仅仅是结构体和类可以实例化,枚举、函数类型和闭包开辟内存空间的过程也可以称为实例化,结果也可以叫“实例”,但不能叫“对象”。
再谈值类型和引用类型
数据类型可以分为:值类型和引用类型,这是由赋值或参数传递方式决定的。值类型就是在赋值或给函数传递参数时候,创建一个副本,把副本传递过去,这样在函数的调用过程中不会影响原始数据。引用类型就是在赋值或给函数传递参数的时候,把本身数据传递过去,这样在函数的调用过程中会影响原始数据。
在众多的数据类型中,只需记住:只有类是引用类型,其他类型全部是值类型。即便结构体与类非常相似,它也是值类型。值类型还包括整型、浮点型、布尔型、字符串、元组、集合和枚举。
Swift中的引用类型与Java中的引用类型是一样的,Java中的类也是引用类型。如果没有Java经验,可以把引用类型理解为C、C++和Objective-C语言中的指针类型,只不过不需要在引用类型变量或常量前面加星号(*)。
示例:
print("--值类型和引用类型--")
var dept = Department()
dept.no = 10
dept.name = "Sales"
var emp = Employee()
emp.no = 1000
emp.name = "Martin"
emp.job = "Salesman"
emp.salary = 1250
emp.dept = dept
func updateDept (inout dept : Department) {
dept.name = "Research"
}
print("Department更新前:\(dept.name)")
updateDept(&dept)
print("Department更新后:\(dept.name)")
func updateEmp (emp : Employee) {
emp.job = "Clerk"
}
print("Employee更新前:\(emp.job)")
updateEmp(emp)
print("Employee更新后:\(emp.job)")
对比上面代码,说明Employee类是引用类型,在调用的时候不用在变量前面添加&符号。
引用类型的比较
恒等于(===)和不恒等于(!===)关系运算符。===用于比较两个引用是否为同一个实例,!===则恰恰相反,它只能用于引用类型,也就是类的实例。
示例:
print("---引用类型的比较---")
var emp1 = Employee() //1
emp1.no = 1000
emp1.name = "Martin"
emp1.job = "Salesman"
emp1.salary = 1250
var emp2 = Employee() //2
emp2.no = 1000
emp2.name = "Martin"
emp2.job = "Salesman"
emp2.salary = 1250
if emp1 === emp2 //3
{
print("emp1 === emp2")
}
if emp1 === emp1 //4
{
print("emp1 === emp1")
}
var dept1 = Department() //5
dept1.no = 10
dept1.name = "Sales"
var dept2 = Department() //6
dept2.no = 10
dept2.name = "Sales"
if dept1 == dept2 //编译失败 //7
{
print("dept1 === dept2")
}
上述代码第1行和第2行分别创建了emp1和emp2两个Employee实例。在代码第3行比较emp1和emp2两个引用是否为一个实例。可以看到,比较结果为False(第3行没有输出emp1 === emp2),也就是emp1和emp2两个引用不是一个实例,即便是它们内容完全一样,结果也是False,而第4行的比较结果为True。如果对于第3行采用==比较,代码如下:
if emp1 == emp1
{
print("emp1 === emp1")
}
答案是有编译错误。==比较要求两个实例的类型(类、结构体、枚举等)必须要在该类型中重写==运算符,定义相等规则。同样的错误也会发生在第7行代码。
第7行使用==比较dept1和dept2两个值是否相等,不仅不能比较,而且还会发生编译错误,这在上面已经解释过了。
如果采用恒等于===比较dept1和dept2,代码如下:
if dept1 === dept2
{
print("dept1 === dept2")
}
这会发生编译错误。===不能比较值类型,而Department结构体是值类型,因此不能使用===比较。
类型嵌套
Swift语言中的类、结构体和枚举可以进行嵌套,即在某一类型的{}内部定义类。这种类型嵌套在Java中称为内部类,在C#中称为嵌套类,它们的形式和设计目的都是类似的。
类型嵌套的优点是能够访问它外部的成员(包括方法、属性和其他的嵌套类型),嵌套还可以有多个层次。
示例:
print("---类型嵌套---")
class Employee {
var no : Int = 0
var name : String = ""
var job : String = ""
var salary : Double = 0
var dept : Department = Department()
var day : WeekDays = WeekDays.Friday
struct Department {
var no : Int = 10
var name : String = "SALES"
}
enum WeekDays {
case Monday
case Tuesday
case Wednesday
case Thursday
case Friday
struct Day {
static var message : String = "Today is ..."
}
}
}
var emp = Employee()
print(emp.dept.name)
print(emp.day)
let friday = Employee.WeekDays.Friday
if emp.day == friday {
print("相等")
}
print(Employee.WeekDays.Day.message)
类型嵌套便于我们访问外部类的成员,但它会使程序结构变得不清楚,使程序的可读性变差。
可选类型与可选链
可选类型
有时使用一个变量或常量,它保存的值可能有也可能没有。示例代码:
print("---可选类型---")
func divide(n1 : Int, n2 : Int) -> Double? {
if n2 == 0 {
return nil
}
return Double(n1) / Double(n2)
}
let result : Double? = divide(100, n2: 200)
可选绑定
可选类型可以用于判断,如下代码:
print("---可选绑定---")
if let result2 : Double? = divide(100, n2: 0) {
print("Success.")
} else {
print("failure.")
}
输出结果为failure.
这种可选类型在if或while语句中赋值并进行判断的写法,叫做可选绑定。
强制拆封
如果我们能确定可选类型一定有值,那么在读取它的时候,可以在可选类型的后面加一个感叹号(!)来获取该值。这种感叹号的表示方式称为可选值的强制拆封(forced unwrapping)。如下代码:
print("---强制拆封---")
let result1 : Double? = divide(100, n2: 200)
print(result1)
print(result1!)//语句中的result1就进行了强制拆封
隐式拆封
为了能够方便地访问可选类型,可以将可选类型后面的问号(?)换成感叹号(!),这种可选类型在拆封时变量或常量后面不加感叹号(!)的表示方式成为隐式拆封。如下代码:
print("--隐私拆封---")
let result3 : Double! = divide(100, n2: 200)
print(result3)
在变量或常量声明的时候,数据类型Double后面跟的是感叹号(!)而不是问号(?),在拆封的时候,变量或常量后面不用加感叹号(!),这就是隐式拆封。隐式拆封的变量或常量使用起来就像普通变量或常量一样,也可以把它看成是普通的变量或常量。
可选链
示例代码:
print("--可选链---")
class Employee {
var no : Int = 0
var name : String = "Tony"
var job : String?
var salary : Double = 0
var dept : Department = Department()
}
class Department {
var no : Int = 10
var name : String = "SALES"
var comp : Company = Company()
}
class Company {
var no : Int = 1000
var name : String = "EOrient"
}
var emp = Employee()
print(emp.dept.comp.name)
Employee通过dept属性与Department关联,Department通过comp属性与Company关联。
通过代码emp.dept.comp.name可以引用到Company实例,形成一个引用的链条,但是这个“链条”任何一个环节“断裂”(为nil)都无法引用到最后的目标(Company实例)。
var dept : Department = Department()是使用Department()构造器实例化dept属性的,这说明给定一个Employee实例,一定会有一个Department与其关联。但是现实世界并非如此,这种关联关系有可能有值,也有可能没有值,我们需要使用可选类型(Department?)声明dept实例。
修改代码如下:
class Employee {
var no : Int = 0
var name : String = "Tony"
var job : String?
var salary : Double = 0
var dept : Department?
}
class Department {
var no : Int = 10
var name : String = "SALES"
var comp : Company?
}
class Company {
var no : Int = 1000
var name : String = "EOrient"
}
var dept : Department?代码声明dept为可选类型,代码var comp : Company?声明comp为可选类型,那么原来的引用方式emp.dept.comp.name已经不能应对可选类型了。之前介绍过可选类型的引用,可以使用感叹号(!)进行强制拆封,代码修改如下:
print(emp.dept!.comp!.name)
但是强制拆封有一个弊端,如果可选链中某个环节为nil,将会导致代码运行时错误。可以采用更加“温柔”的引用方式,使用问号(?)来代替原来感叹号(!)的位置。如下所示:
print(emp.dept?.comp?.name)
问号(?)表示引用的时候,如果某个环节为nil,它不会抛出错误,而是会把nil返回给引用者。这种由问号(?)引用可选类型的方式就是可选链。
可选链是一种“温柔”的引用方式,它的引用目标不仅仅是属性,还可以是方法、下标和嵌套类型等。
具有嵌套类型的示例:
class Employee {
var no : Int = 0
var name : String = ""
var job : String = ""
var salary : Double = 0
var dept : Department?
struct Department {
var no : Int = 10
var name : String = "SALES"
}
}
var emp = Employee()
print(emp.dept?.name)
代码var dept : Department?定义可选类型Department?的属性dept,Department是嵌套结构体类型。print(emp.dept?.name)采用可选链方式引用。输出结果为nil,这是因为emp.dept环节为nil。如果将代码var dept : Department?修改一下:
var dept : Department? = Department()
则输出结果为SALES。这说明可选链可以到达目标name。
1.可选类型中的问号(?)
声明这个类型是可选类型,访问这种类型的变量和常量时要使用感叹号(!),下列代码是强制拆封:
let result1 : Double? = divide(100, 200)
print(result1!)
2.可选类型中的感叹号(!)
声明这个类型也是可选类型,但是访问这种类型的变量或常量时可以不使用感叹号(!),下列代码是隐式拆封:
let result3 : Double? = divide(100, 200)
print(result3)
3.可选链中的感叹号(!)
多个对象具有关联关系,当从一个对象引用另外对象的方式、属性和下标等成员时就会形成引用连,由于这个“链条”某些环节可能有值,也可能没有值,因此需要采用如下方式访问:
emp.dept!.comp!.name
4.可选链中的问号(?)
在可选链中使用感叹号(!)访问时,一旦“链条”某些环节没有值,程序就会发生异常,于是我们把感叹号(!)改为问号(?)
,代码如下所示:
emp.dept?.comp?.name
这样某些环节没有值的时候返回nil,程序不会发生异常。
访问限定
作为一种面向对象的语言封装性是不可缺少的,Swift语言在正式版中增加了访问控制,这样一来Swift语言就可以实现封装特性了。由于在Swift语言中类、结构体和枚举类型都具有面向对象的特性,因此Swift语言的封装就变得比较复杂。
访问范围
访问范围主要有两个: 模块和源文件。
模块是指一个应用程序包或一个框架。在Swift中,可以用import关键字将模块引入到自己的工程中。应用程序包是可执行的,其内部包含了很多Swift文件以及其他文件。
框架也是很多Swift文件及其他文件的集合,但是应用程序包不同的是,它编译的结果是不可以执行文件。
源文件指的是Swift中的.swift文件,编译之后它被包含在应用程序包或框架中,通常一个源文件包含一个面向对象类型(类、结构体和枚举),在这些类型中又包含函数、属性等。
访问级别
Swift提供了3种不同访问级别,对应的访问修饰符为:public、internal和private。这些访问修饰符可以修饰类、结构体、枚举等面向对象的类型,还可以修饰变量、常量、下标、元组、函数、属性等内容。
提示:为了便于描述,我们把类、结构体、枚举、变量、常量、下标、元组、函数、属性等内容统一称为“实体”。
public 可以访问自己模块中的任何public实体。如果使用import语句引入其他模块,可以访问其他模块中的public实体。
internal 只能访问自己模块的任何internal实体,不能访问其他模块中的internal实体。internal可以省略,换句话说,默认访问限定是internal
private 只能在当前源文件中使用的实体,称为私有实体。使用private修饰,可以用作隐藏某些功能的实现细节。
使用访问修饰符的示例代码如下:
print("--使用访问修饰符示例代码---")
public class PublicClass {}
internal class InternalClass {}
private class PrivateClass {}
public var intPublicVariable = 0
let intInternalConstant = 0//internal访问级别
private func intPrivateFunction()
使用访问级别最佳实践
1.统一性原则
- 原则1:如果一个类型(类、结构体、枚举)定义为internal或private,那么类型声明的变量或常量不能使用public访问级别。因为public的变量或常量可以被任何人访问,而internal或private的类型不可以。
代码:
print("--原则1--")
private class Employee {
var no : Int = 0
var name : String = ""
var job : String?
var salary : Double = 0
var dept : Department?
}
internal struct Department {
var no : Int = 0
var name : String = ""
}
public let emp = Employee()//编译错误
public var dept = Department()//编译错误
2.设计原则
如果编写的是应用程序,应用程序包中的所有Swift文件和其中定义的实体,都是供本应用使用的,而不是提供其他模块使用,那么就不用设置访问级别了,即使用默认的访问级别。
如果开发的是框架,框架编译的文件不能独立运行,因此它天生就是给别人使用的,这种情况下要详细设计其中的Swift文件和实体的访问级别,让别人使用的可以设定为public,不想让别人看到的可以设定为internal或private。
3.元组类型的访问级别
元组类型的访问级别遵循元组中字段最低级的访问级别。代码如下:
print("---元组类型的访问级别---")
private class Employee {
var no : Int = 0
var name : String = ""
var job : String?
var salary : Double = 0
var dept : Department?
}
struct Department {
var no : Int = 0
var name : String = ""
}
private let emp = Employee()
var dept = Department()
private var student1 = (dept, emp)
private var student1 = (dept, emp)定义了元组student1,其中的字段dept和emp的最低访问级别是private,所以student1访问级别也是private,这也符合统一性原则。
4.枚举类型的访问级别
枚举中成员的访问级别继承自该枚举,因此不能为枚举中的成员指定访问级别。示例代码如下:
print("---枚举类型的访问级别---")
public enum WeekDays {
case Monday
case Tuesday
case Wednesday
case Thursday
case Friday
}
由于WeekDays枚举类型是public访问级别,因而它的成员也是public级别。