知道 ObjectMapper
的人大概都见过在使用 Mappable
定义的模型中 func mapping(map: Map) {}
中须要写不少 name <- map["name"]
这样的代码。这里的 <-
将模型中的属性跟数据中的 key
对应了起来。git
Swift 提供的这种特性可以减小不少的代码量,也能极大的简化语法。在标准库或者是咱们本身定义的一些类型中,有一些只是简单的一些基本的值类型的容器,好比说 CGRect
、CGSize
、CGPoint
这些东西。或者直接使用 John Sundell 的文章 Custom operators in Swift 中的例子。在某个策略类游戏中,玩家可以收集两种资源木材还有金币。为了要将两种资源模型化,定义了 Resources
这个结构体。github
struct Resources {
var gold: Int
var wood: Int
}
复制代码
固然这些资源都是一个具体的玩家来使用或者赚取的。express
struct Player {
var resources: Resources
}
复制代码
用户能够经过训练军队来使用这些资源。当用户训练军队的时候,都须要从用户的 resources
里面减去对应数量的金币还有木材。好比用户花费10个金币20个木材训练了一个弓箭手(Archer
)。swift
咱们先定义弓箭手这个容器:api
protocol Armyable {
var cost: Resources { get }
}
struct Archer: Armyable {
var cost: Resources = Resources(gold: 10, wood: 20)
}
复制代码
在这个例子中咱们首先定义了Armyable
这个协议来描述全部的军队类型。固然在这个例子里面只有训练花费的资源也就是 cost
这一个东西。Archer
这个结构体直接定义了训练一个弓箭手须要耗费的资源量。数组
如今再在 Player
这个方法里面定义训练军队的方法。安全
var board: [String]
mutating func trainArmy(_ unit: Armyable) {
resources.gold -= unit.cost.gold // line 1
resources.wood -= unit.cost.wood // line 2
board.append("弓箭手")
}
复制代码
首先模拟的定义了一个数组来存放当前的军队。而后定义了 trainArmy
这个方法来训练军队。这样就完成了训练军队这个逻辑的编码工做。可是可能你也想到了,在这类游戏中,有不少的状况须要操做用户的资源,也就是说上面 line1 line2 之类的代码会在这个游戏里写不少次。若是你以为只是重复写点代码没什么的话,那么之后须要新增另外的什么资源的时候呢?恐怕就只能在整个代码库中找到全部相关的地方了。app
这时候要是可以用到数学符号 +
、-
就完美了。Swift 也替咱们想到了这点。咱们能够本身定义一个操做符也能够重载一个已经有了的操做符。操做符重载跟方法重载同样。咱们先重载 -=
这个符号。布局
extension Resources {
static func -= (lhs: inout Resources, rhs: Resources) {
lhs.gold -= rhs.gold
lhs.wood -= rhs.wood
}
}
复制代码
跟 Equatable
同样,Swift 中的操做符重载只是一个简单的静态方法。在 -=
这个方法里面,左边的参数被标记成了inout
, 这个参数就是咱们须要改变的值。有了 -=
这个操做符,咱们如今就能够像操做数字同样操做 resourcepost
resources -= unit.cost
复制代码
这么些不只仅看起来或者读起来很友好,也可以帮助咱们减小相似的代码处处 copy 的问题。既然如今咱们可使用外部逻辑改变 resource ,如今甚至能够把 Resource 中的属性改为只读的。
struct Resources {
private(set) var gold: Int
private(set) var wood: Int
init(gold: Int, wood: Int) {
self.gold = gold
self.wood = wood
}
}
复制代码
固然咱们也可使用 mutating
方法来作这件事情。
extension Resources {
mutating func reduce(by resources: Resources) {
gold -= resources.gold
wood -= resources.wood
}
}
复制代码
上面两种方法都各有优点,你能够说使用 mutating 方法可让读者更加明确代码的含义。可是你确定也不想标准库中的减法变成
5.reduce(by: 3)
这样的。
还有一个场景就是刚刚提到了作 UI 布局的时候,涉及到的 CGRect、 CGPoint 等等。在作布局的时候常常会涉及到须要对这些值进行运算,若是可以使用像上面那样的方法来作这件事情不是很好的吗?
extension CGSize {
static func + (lhs: CGSize, rhs: CGSize) -> CGPoint {
return CGPoint(x: lhs.width + rhs.width,
y: lhs.height + rhs.height)
}
}
复制代码
这段代码,重载了 +
这个操做符,接受两个 CGSize, 返回 CGPoint。而后就能够这样写了
label.frame.origin = imageView.bounds.size + CGSize(width: 10, height: 20)
复制代码
这样已经很好的,可是必需要建立一个 CGSize 对象确实还不够好。因此咱们再多定义一个 +
这个操做符接受一个元组:
extension CGSize {
static func + (lhs: CGSize, rhs: (x: CGFloat, y: CGFloat)) -> CGPoint {
return CGPoint(
x: lhs.width + rhs.x,
y: lhs.height + rhs.y)
}
}
复制代码
而后就能够把上面的代码进一步简化了:
label.frame.origin = imageView.bounds.size + (x: 10, y: 20)
// or
label.frame.origin = imageView.bounds.size + (10,20)
复制代码
知道如今咱们都还在操做数字相关的东西,大多数的人都可以很轻松的去理解和阅读这些代码,可是若是是在涉及到一些特别的点,特别是须要引入新的操做符的时候,就须要好好去思考这样作的必要性的。这是一个关于冗余代码和可读性代码的关键点。
做者 John Sundel 有一个库 CGOperators 是不少关于 Core Graphics 中的类的。
到如今,咱们已经知道了如何去重载已有的操做符。有些时候咱们还想要使用操做符来作一些操做,而在已经存在的操做符中找不到对应的,这种时候就须要本身去定义一个操做符了。
咱们来举个例子。 Swift 中的 do
、try
、 catch
是很是好的异常处理机制。它让咱们可以很安全的从发生了异常的方法里退出,好比说下面这个从本地读取数据的例子:
class NoteManager {
func loadNote(fromFileNamed fileName: String) throws -> Note {
let file = try fileLoader.loadFile(named: fileName)
let data = try file.read()
let note = try Note(data: data)
return note
}
}
复制代码
这么些最大的缺陷就是在遇到异常的时候,咱们给调用者直接抛出了比较隐晦的异常。*“Providing a unified Swift error API” 这篇文章聊过减小一个 API 可以抛出异常的总量的好处。
这种状况下,咱们想要的异常实际上是有限的,这样咱们就可以很轻松的处理每一种异常状况。可是,咱们仍是像捕获到全部的异常,得到每一个异常的消息,咱们能够定义一个枚举:
extension NoteManager {
enum LoadingError: Error {
case invalidFile(Error)
case invalidData(Error)
case decodingFailed(Error)
}
}
复制代码
这样就能够将各类异常消息归类,而且不会影响到外界知道这个错误的具体信息。可是这样写代码就会变成这样了:
class NoteManager {
func loadNote(fromFileNamed fileName: String) throws -> Note {
do {
let file = try fileLoader.loadFile(named: fileName)
do {
let data = try file.read()
do {
return try Note(data: data)
} catch {
throw LoadingError.decodingFailed(error)
}
} catch {
throw LoadingError.invalidData(error)
}
} catch {
throw LoadingError.invalidFile(error)
}
}
}
复制代码
不得不说这简直就是一场灾难。相信没人愿意读到这样的代码吧!引入一个新的操做 perform
可让代码看起来更友好一些:
class NoteManager {
func loadNote(fromFileNamed fileName: String) throws -> Note {
let file = try perform(fileLoader.loadFile(named: fileName),
orThrow: LoadingError.invalidFile)
let data = try perform(file.read(),
orThrow: LoadingError.invalidData)
let note = try perform(Note(data: data),
orThrow: LoadingError.decodingFailed)
return note
}
}
复制代码
这就好不少了,可是依然有不少异常处理相关的代码会干扰主逻辑。下面咱们来看看引入新的操做符以后会是什么样的状况。
咱们如今来自定义一个操做符。我选择了 ~>
。
infix operator ~>
复制代码
prefix operator &*& {} //定义左操做符
infix operator ** {} //定义中操做符
postfix operator && {} //定义右操做符
prefix func &*&(a: Int) -> Int { ... }
postfix func &&(a: Int) -> Int { ... }
// let c = 1&&
// let b = &*&1
// let a = 1 ** 2
复制代码
操做符可以如此强大的缘由在于它可以捕获到两边的上下文。结合 Swift 的 @autoclosure
特性咱们就能够作一些很酷的事情了。
请咱们来实现这个操做符吧!让它接受一个可以抛出一场的表达式,以及一个异常转换的表达式。返回原来的值或者是原来的异常。
func ~><T>(expression: @autoclosure () throws -> T,
errorTransform: (Error) -> Error) throws -> T {
do {
return try expression()
} catch {
throw errorTransform(error)
}
}
复制代码
这一段代码可以让咱们很够简单的经过在操做和异常之间添加 ~>
来表达具体执行的任务以及可能遇到的异常。以前的代码就能够改为这样了:
class NoteManager {
func loadNote(fromFileNamed fileName: String) throws -> Note {
let file = try fileLoader.loadFile(named: fileName) ~> LoadingError.invalidFile
let data = try file.read() ~> LoadingError.invalidData
let note = try Note(data: data) ~> LoadingError.decodingFailed
return note
}
}
复制代码
怎么样,经过引入一个操做符,咱们能够移除掉不少干扰阅读的代码。可是缺点就是,因为引入了新的操做符,这对新人来讲,这会是额外的学习成本。
自定义操做符以及操做符重载是 Swift 中一个很强大的特性,它可以帮助你很轻松的去构建一些解决方案。它可以帮助咱们减小在类似逻辑中的代码复制,让代码更干净。可是它也可能会让你一不当心就写出了隐晦,阅读不友好的代码。
在引入自定义操做符或者是想要重载某个操做符的时候,仍是须要好好想想利弊。从其余同事或者同行那里寻求建议是一个很是有效的方法,新的操做符对你本身来讲可能很好,可是别人看起来可能会以为很奇怪。同其余不少的事情同样,这其实就是一个关于权衡的话题,咱们须要为每种状况选择最合适的解决方案。