Swift 中关于操做符的那些事儿

知道 ObjectMapper 的人大概都见过在使用 Mappable 定义的模型中 func mapping(map: Map) {} 中须要写不少 name <- map["name"] 这样的代码。这里的 <- 将模型中的属性跟数据中的 key 对应了起来。git

Swift 提供的这种特性可以减小不少的代码量,也能极大的简化语法。在标准库或者是咱们本身定义的一些类型中,有一些只是简单的一些基本的值类型的容器,好比说 CGRectCGSizeCGPoint 这些东西。或者直接使用 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 中的 dotrycatch 是很是好的异常处理机制。它让咱们可以很安全的从发生了异常的方法里退出,好比说下面这个从本地读取数据的例子:

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 中一个很强大的特性,它可以帮助你很轻松的去构建一些解决方案。它可以帮助咱们减小在类似逻辑中的代码复制,让代码更干净。可是它也可能会让你一不当心就写出了隐晦,阅读不友好的代码。

在引入自定义操做符或者是想要重载某个操做符的时候,仍是须要好好想想利弊。从其余同事或者同行那里寻求建议是一个很是有效的方法,新的操做符对你本身来讲可能很好,可是别人看起来可能会以为很奇怪。同其余不少的事情同样,这其实就是一个关于权衡的话题,咱们须要为每种状况选择最合适的解决方案。

相关文章
相关标签/搜索