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

综合技术 稀土掘金 (源链)

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

Swift 提供的这种特性能够减少很多的代码量,也能极大的简化语法。在标准库或者是我们自己定义的一些类型中,有一些只是简单的一些基本的值类型的容器,比如说 CGRect
CGSize
CGPoint
这些东西。或者直接使用John Sundell 的文章 Custom operators in Swift
中的例子。在某个策略类游戏中,玩家能够手机两种资源木材还有金币。为了要将两种资源模型化,定义了 Resources
这个结构体。

struct Resources {
    var gold: Int
    var wood: Int
}

当然这些资源都是一个具体的玩家来使用或者赚取的。

struct Player {
    var resources: Resources
}

用户可以通过训练军队来使用这些资源。当用户训练军队的时候,都需要从用户的 resources
里面减去对应数量的金币还有木材。比如用户花费10个金币20个木材训练了一个弓箭手( Archer
)。

我们先定义弓箭手这个容器:

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 之类的代码会在这个游戏里写很多次。如果你觉得只是重复写点代码没什么的话,那么以后需要新增另外的什么资源的时候呢?恐怕就只能在整个代码库中找到所有相关的地方了。

操作符重载

这时候要是能够用到数学符号 +
-
就完美了。Swift 也替我们想到了这点。我们可以自己定义一个操作符也可以重载一个已经有了的操作符。操作符重载跟方法重载一样。我们先重载 -=
这个符号。

extension Resources {
    static func -= (lhs: inout Resources, rhs: Resources) {
        lhs.gold -= rhs.gold
        lhs.wood -= rhs.wood
    }
}

Equatable
一样,Swift 中的操作符重载只是一个简单的静态方法。在 -=
这个方法里面,左边的参数被标记成了 inout
, 这个参数就是我们需要改变的值。有了 -=
这个操作符,我们现在就可以像操作数字一样操作 resource

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

在引入自定义操作符或者是想要重载某个操作符的时候,还是需要好好想一想利弊。从其他同事或者同行那里寻求建议是一个非常有效的方法,新的操作符对你自己来说可能很好,但是别人看起来可能会觉得很奇怪。 同其他很多的事情一样,这其实就是一个关于权衡的话题,我们需要为每种情况选择最合适的解决方案。

您可能感兴趣的

又立 Flag ?Swift 5 开发启动称必须实现 ABI 稳定... 前几日,Swift 语言开发项目组主管 Ted Kremenek 发 邮件 称,Swift 4 更新工作已基本完结,将在今年晚些时候正式发布。同时,Swift 5 的开发工作即将展开,鼓励开发者提交提案。 Kremenek 在 Swift Evolution 中更新了 README.md...
Migrating to Swift 3 The iOS team at Nextdoor recently migrated our iOS codebase to Swift 3. In this blog post, we wanted to share the approach we took and learnings from ...
用Swift实现Dijkstra算法 如果你以前听说过图论, 那么你熟悉Dijkstra算法, 如果你不熟悉,那么好, 这篇文章包含了你所需要知道的一切 快速介绍 这个章节将带你快读过一下什么是图论和Dijkstra算法 如果你够自信,你可以跳过这个部分(直接跳到Swift章节) 图论 看到文章开头的...
Instance property vs parameter in Swift Original post https://github.com/onmyway133/blog/issues/72 The other day I was refactor my code. I have extension MainController: TabBarVie...
What future for Apple’s Swift? Back in 2015, on the WWDC stage, Craig Federighi announced to the audience that Swift would become open source . The statement took a lot of peopl...
稀土掘金责编内容来自:稀土掘金 (源链) | 更多关于

阅读提示:酷辣虫无法对本内容的真实性提供任何保证,请自行验证并承担相关的风险与后果!
本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » Swift 中关于操作符的那些事儿



专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录