面向对象编程(OOP,Object Oriented Programing)有三大特性:
- 继承
- 封装
- 多态
在Swift中,面向对象的基本单元如下:
- 枚举
- 结构体
- 类
- 协议
- 扩展
枚举
在Swift中,枚举与类、结构体具有完全平等的地位。
关联值
我们可以定义Swift枚举来存储任意给定类型的关联值,不同的枚举成员关联值的类型是可以不同的。
比如,现在有一个库存系统,库存系统中标签有两种:
- 条形码,存储四组数字
- 二维码,存储一个字符串
var productBarCode = BarCode.upc(8, 67895, 86532, 6)print(productBarCode) // upc(8, 67895, 86532, 6)productBarCode = BarCode.qrCode("DMGJH<SGHK(*&CMVS")print(productBarCode) // qrCode("DMGJH<SGHK(*&CMVS")
我们也可以使用值绑定的方式来得到所有的关联值是什么,如下:
代码语言:javascript复制let productBarCode = BarCode.upc(8, 67895, 86532, 6)switch productBarCode {case .upc(let numberSystem, let manufacturer, let product, let check): print("UPC:(numberSystem), (manufacturer), (product), (check)")case .qrCode(let productCode): print("QRCODE:(productCode)")}
打印结果如下:UPC:8, 67895, 86532, 6
递归枚举
递归枚举是拥有另一个枚举作为枚举成员关联值的枚举,编译器在操作递归枚举时必须插入间接寻址层,你可以在声明枚举成员之前使用indirect关键字来明确它是递归的。
(5 4)*2的表达如下:
代码语言:javascript复制indirect enum ArithmeticExpression { case number(Int) case addition(ArithmeticExpression, ArithmeticExpression) case multiplication(ArithmeticExpression, ArithmeticExpression) var result: Int { switch self { case .number(let value): return value case .addition(let expressionLeft, let expressionRight): return expressionLeft.result expressionRight.result case .multiplication(let expressionLeft, let expressionRight): return expressionLeft.result * expressionRight.result } }}
let five = ArithmeticExpression.number(5)let four = ArithmeticExpression.number(4)let sum = ArithmeticExpression.addition(five, four)let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))print(product.result) // 18
像访问数组和字典一样访问类和结构体:下标和下标重载
下标脚本允许你通过在实例名后面的方括号内写一个或者多个值来对该类的实例进行查询。
我们知道,数组、字典等都是可以通过下标来访问里面的元素的。比如,数组可以通过Int类型的下标访问其中的某个元素,字典可以通过Key类型的下标访问到某个具体值。
实际上,在Swift中,所有的类、结构体和枚举都是可以定义下标操作的,它可以作为访问集合、列表或序列成员元素的快捷方式。你可使用下标通过索引值来设置或者检索值,而不需要为设置和检索分别使用实例方法。
你可以为一个类型定义多个下标,并且下标会基于传入的索引值的类型选择合适的下标重载来使用。下标没有限制单个维度,你可以使用多个输入形参来定义下标以满足自定义类型的需求。
我们使用关键字subsript来定义下标,并且指定一个或者多个输入形式参数和返回类型,这与实例方法是一样的。与实例方法不同的是,下标可以是读写,也可以是只读的,如果只有get方法,那么就是只读,如果get和set都有,那么就是读写。
下标可以接收任意数量的输入形式参数,并且这些输入形式参数可以是任意类型。下标也可以返回任意类型。需要注意的是,下标是不可以提供默认的形式参数值的。
代码语言:javascript复制struct Matrix { // 矩阵 let rows: Int, columns: Int // 行数和列数 var grid: [Double] // 存储矩阵中每个位置上的值 init(rows: Int, columns: Int) { self.rows = rows self.columns = columns grid = Array(repeating: 0.0, count: rows * columns) } func indexIsValid(row: Int, column: Int) -> Bool { return row >= 0 && row < rows && column >= 0 && column < columns } //定义下标操作 subscript (row: Int, column: Int) -> Double { get { assert(indexIsValid(row: row, column: column), "Index Out Of Range") return grid[(row * columns) column] } set { assert(indexIsValid(row: row, column: column), "Index Out Of Range") grid[(row * columns) column] = newValue } }}
使用如下:var matrix = Matrix(rows: 2, columns: 2)// 使用下标操作对对应位置进行赋值matrix[0, 1] = 1.5matrix[1, 0] = 3print(matrix) // Matrix(rows: 2, columns: 2, grid: [0.0, 1.5, 3.0, 0.0])// 使用下标操作来得到对应位置上的值print(matrix[0,0]) // 0.0
Matrix是一个矩阵结构体。
rows和columns分别是列数和行数。
使用数组grid来存储矩阵中每个元素的值。
初始化的时候会传入函数和列数,并且每一个元素都会被初始化为0.0。
如上文描述,你可以在对应类型的实例上调用下标,此为实例下标。
同样地,你也可以定义类型本身的下标,这类下标叫做类型下标。你可以在subscript关键字前加上static关键字来标记类型下标,如果是在类中,则还可以使用class关键字,这样可以允许子类重写父类的下标实现。如下:
enum CompassPoint: Int { case south = 1 case north case west case east // 类型下标 static subscript(index: Int) -> CompassPoint { get { return CompassPoint(rawValue: index)! } }}
使用如下:print(CompassPoint[2]) // north
类的两段式初始化
Swift中类的初始化是一个两段式过程:
- 在第一个阶段,每一个存储属性被引入类分配了一个初始值。一旦每个存储属性的初始状态都被确定,第一个阶段就会结束,第二个阶段就开始了。
- 在第二个阶段,每个类都有机会在新的实例准备使用之前来定制它的存储属性。
两段式初始化中的安全检查
- 指定初始化器必须保证,在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
- 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将会被父类的初始化器所覆盖。
- 便捷初始化器必须先委托同类中的其他初始化器,然后再为任意属性(包括同类里定义的属性)赋新值。如果不这样做,便捷初始化器赋予的新值都将被自己类中的其他指定初始化器所覆盖。
- 初始化器在第一阶段初始化完成之前,不能调用任何实例方法,不能读取任何实例属性的值,也不能引用self作为值。
两段式初始化过程
阶段一
- 指定或者便捷初始化器在类中被调用
- 为这个类的新实例分配内存。此时,内存还没有被初始化
- 这个类的指定初始化器确保所有由此类引入的存储属性都有一个值。此时,这些存储属性的内存被初始化了
- 指定初始化器向上委托给父类的初始化器,使父类为其存储属性执行相同的任务
- 这个调用父类初始化器的过程将沿着初始化器链一直向上进行,直到到达初始化器链的最顶部
- 一旦到达初始化器链的最顶部,在链顶部的类会确保所有存储属性都有一个值,此时的内存会被认为完全初始化了,此时第一阶段完成
阶段二
- 从顶部初始化器往下,链中的每一个指定初始化器都有机会进一步定制实例。初始化器现在能够访问self,并且可以修改它的属性、调用它的实例方法等等。
- 最终,链中任何便利初始化器都有机会定制实例,以及使用self
扩展和协议
扩展
extension的能力如下:
- 添加计算实例属性和计算类型属性
- 定义实例方法和类型方法
- 提供新的初始化器
- 使现有类型遵循某个协议
- 给协议提供默认实现
- 定义下标
- 定义和使用新的内嵌类型
需要注意如下一点:
扩展可以添加计算属性,但是不能添加存储属性。因为类型在被实例化之后,其内存空间就已经确定了,而添加存储属性需要内存空间,这会改变原有的内存结构。可以类比OC中分类不能添加属性。
协议
协议是可以作为类型来使用的:
- 在函数、方法或者初始化器里面作为形式参数类型或者返回值类型
- 作为常量、变量或者属性的类型
- 作为数组、字典或者其他存储器的元素的类型
我们可以通过添加AnyObject关键字到协议的继承列表,来限制协议只能被类类型采纳(即,不可以被枚举、结构体等值类型遵循):
代码语言:javascript复制protocol SomeClassOnlyProtocol: AnyObject { // class-only protocol definition goes here}
我们可以使用协议组合来复合多个协议到一个要求里。你可以将协议组合行为理解为你定义的临时局部协议,这个临时局部协议会拥有组合中所有协议的要求。需要注意的是,协议组合不会定义任何新的协议类型。
协议组合会使用&符号来连接任意数量的协议。除了协议列表,协议组合也能包含类类型,这允许你标明一个需要的父类。如下:
扩展与协议的结合
有条件地遵循协议
我们知道,可以通过扩展来给一个已经存在的类型遵循新的协议。那么如果这个类型是泛型,那么可能会只在某些情况下满足一个协议的要求,比如,当类型的泛型形式参数遵循对应协议的时候。我们可以通过在扩展类型时列出限制让泛型类型有条件地遵循某协议,语法就是,在你所要遵循的协议的名字后面写泛型where分句。
面向协议编程
几乎所有的语言都会支持OOP,OOP的设计理念就是万物皆对象。
OOP我们都很熟悉了,现在来聊聊OOP的缺陷:
- 面向对象中的继承机制会要求你在开发之前就要设计好整个程序的框架、结构、事物间的连接关系。这要求开发者必须有很好的的分类设计能力,能够将不同的属性和方法分配到合适的层次里面去。设计清晰明了的继承体系总是很难的。
- 上面也提到,OOP会在开始之前确定整个框架结构,而结构天生对改动是有抵抗性的。OOP领域中所有程序猿都对重构讳莫如深,因为修改结构的影响点很多,我们考虑的东西也很多,修改行为总是比修改结构要简单的。
- 继承机制带来的另外一个问题是:很多语言都不提供多继承,我们不得不在父类塞入更多内容,子类中会存在一些无用的父类属性和方法,这些冗余的代码会给子类带来一定的不经意调用的风险,而且对于层级很深的结构而言,查找Bug会更加复杂。因此,组合总是优于继承的。
- 类的对象的状态在分享和传递过程中是很难调试的,尤其是在并行程序编码中,该问题就更加明显。比如一个类的对象经过20层传递,当最后该对象的值出现了问题的时候,我们需要向上一层一层查找是哪一层出现了问题,即便如此,这要是在串行情形下还是不难排查的,如果是在并行,那么就更难排查了。OOP所带来的可变、不确定、复杂等特性,与并行编程所倡导的小型化、核心化、高效化完全背离。因此,在并行编程中,值类型总是优于引用类型的,因为值类型传递过去之后,我就不需要管了,后面该值所有的变化对我都没有影响了。
好,了解完OOP,现在就开始聊聊POP。
POP,Protocol Oriented Programming,面向协议编程。
下面让我们来比较一下OC和Swift中的Array:
如上图,左边是OC中的数组的继承体系,右边是Swift中的数组。
我们可以看到,在Objective-C中,可变数组NSMutableArray继承自NSArray,NSArray除了遵循NSCopying等通用协议之外,还继承自基类NSObject。实际上,NSArray虽然遵循了一些协议,但是这些协议基本都是通用协议,数组的一些功能大部分还是集中在NSArray这个类里面定义和实现的。
在Swift中,Array会遵循非常多的协议,Array的每一小块功能都会有对应的协议来对应,Array通过遵循这一系列的协议,最终构成了Array这个类型。
OOP VS POP
OOP主要关心对象是什么;POP主要关心对象做什么。
下面看个例子:
class Human { var name: String var age: Int init(name: String, age: Int) { self.name = name self.age = age } func sayHi() { print("say hi") }}
class Athlete: Human { override func sayHi() { print("Hi,im (name)") }}
class Runner: Athlete { func run() { print("run") }}
class Swimmer: Athlete { func swim() { print("swim") }}
这里我定义了一个Human类型,Human有两个基本属性name和age,和一个基本行为sayHi。
还定义了一个运动员类型Athlete,Athlete继承自Human类型,并重新定义了sayHi行为。
运动员Athlete包括很多种,比如说田径运动员Runner,游泳运动员Swimmer等。此时我们考虑一下,如果有一个运动员比较全能,他既是田径运动员,又是游泳运动员,此时该怎么办呢?我们知道,大部分的语言都是不支持多继承的,因此这个时候对于程序员而言,使用OOP就比较难处理这种情形了。
接下来我们看一下使用POP是怎么解决这个问题的:
代码语言:javascript复制protocol Human { var name: String { get set } var age: Int { get set } func sayHi()}
protocol Runnable { func run()}
protocol Swimming { func swim()}
struct Runner: Human, Runnable { var name: String var age: Int func sayHi() { print("Hi, I'm (name)") } func run() { print("Run") }}
struct Swimmer: Human, Swimming { var name: String var age: Int func sayHi() { print("Hi, I'm (name)") } func swim() { print("swim") }}
// 全能运动员struct AllAroundAthlete: Human, Swimming, Runnable { var name: String var age: Int func sayHi() { print("Hi, I'm (name)") } func swim() { print("swim") } func run() { print("run") }}
此时,Human是一个协议,它有两个属性约束name和age,以及一个方法约束sayHi;Runnable协议定义了的run行为约束;Swimming协议定义了swim的行为约束。然后通过协议的遵循来创造一个全能运动员。
以上。