面向对象编程(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")
我们也可以使用值绑定的方式来得到所有的关联值是什么,如下:
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的表达如下:
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都有,那么就是读写。
下标可以接收任意数量的输入形式参数,并且这些输入形式参数可以是任意类型。下标也可以返回任意类型。需要注意的是,下标是不可以提供默认的形式参数值的。
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中类的初始化是一个两段式过程:
两段式初始化中的安全检查
两段式初始化过程
阶段一
阶段二
扩展和协议
扩展
extension的能力如下:
需要注意如下一点:
扩展可以添加计算属性,但是不能添加存储属性。因为类型在被实例化之后,其内存空间就已经确定了,而添加存储属性需要内存空间,这会改变原有的内存结构。可以类比OC中分类不能添加属性。
协议
协议是可以作为类型来使用的:
我们可以通过添加AnyObject关键字到协议的继承列表,来限制协议只能被类类型采纳(即,不可以被枚举、结构体等值类型遵循):
protocol SomeClassOnlyProtocol: AnyObject { // class-only protocol definition goes here}
我们可以使用协议组合来复合多个协议到一个要求里。你可以将协议组合行为理解为你定义的临时局部协议,这个临时局部协议会拥有组合中所有协议的要求。需要注意的是,协议组合不会定义任何新的协议类型。
协议组合会使用&符号来连接任意数量的协议。除了协议列表,协议组合也能包含类类型,这允许你标明一个需要的父类。如下:
扩展与协议的结合
有条件地遵循协议
我们知道,可以通过扩展来给一个已经存在的类型遵循新的协议。那么如果这个类型是泛型,那么可能会只在某些情况下满足一个协议的要求,比如,当类型的泛型形式参数遵循对应协议的时候。我们可以通过在扩展类型时列出限制让泛型类型有条件地遵循某协议,语法就是,在你所要遵循的协议的名字后面写泛型where分句。
面向协议编程
几乎所有的语言都会支持OOP,OOP的设计理念就是万物皆对象。
OOP我们都很熟悉了,现在来聊聊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是怎么解决这个问题的:
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的行为约束。然后通过协议的遵循来创造一个全能运动员。
以上。