Swift之深入解析如何自定义操作符

Posted Forever_wj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift之深入解析如何自定义操作符相关的知识,希望对你有一定的参考价值。

一、Swift 运算符

  • 运算符是一个符号,用于告诉编译器执行一个数学或逻辑运算。
  • Swift 提供了以下几种运算符:
    • 算术运算符
    • 比较运算符
    • 逻辑运算符
    • 位运算符
    • 赋值运算符
    • 区间运算符
    • 其它运算符

① 算术运算符

  • 如下所示,列出了 Swift 语言支持的算术运算符,其中变量 A 为 10,变量 B 为 20:
运算符描述实例
+加号A + B 结果为 30
减号A − B 结果为 -10
*乘号A * B 结果为 200
/除号B / A 结果为 2
%求余B % A 结果为 0
  • Swift 中已经取消 ++、–。

② 比较运算符

  • 如下所示,列出了 Swift 语言支持的比较运算符,其中变量 A 为 10,变量 B 为 20:
运算符描述实例
==等于(A == B) 为 false
!=不等于(A != B) 为 true
>大于(A > B) 为 false
<小于(A < B) 为 true
>=大于等于(A >= B) 为 false
<=小于等于(A <= B) 为 true

③ 逻辑运算符

  • 如下所示,列出了 Swift 语言支持的逻辑运算符,其中变量 A 为 true,变量 B 为 false:
运算符描述实例
&&逻辑与,如果运算符两侧都为 TRUE 则为 TRUE(A && B) 为 false
||逻辑或,如果运算符两侧至少有一个为 TRUE 则为 TRUE(A c| B) 为 true
!逻辑非,布尔值取反,使得true变false,false变true!(A && B) 为 true

④ 位运算符

  • 位运算符用来对二进制位进行操作,~,&,|,^ 分别为取反,按位与与,按位与或,按位与异或运算,如下表:
pqp & qp | qp ^ q
00000
01011
11110
10011
  • 如果指定 A = 60; 及 B = 13; 两个变量对应的二进制为:
A = 0011 1100

B = 0000 1101
  • 进行位运算:

⑤ 赋值运算

  • 如下所示,列出了 Swift 语言的基本赋值运算:

⑥ 区间运算符

  • Swift 提供了两个区间的运算符:
运算符描述实例
闭区间运算符闭区间运算符(a…b)定义一个包含从a到b(包括a和b)的所有值的区间,b必须大于等于a。 ‌ 闭区间运算符在迭代一个区间的所有值时是非常有用的,如在for-in循环中1…5 区间值为 1, 2, 3, 4 和 5
半开区间运算符半开区间(a…<b)定义一个从a到b但不包括b的区间。 之所以称为半开区间,是因为该区间包含第一个值而不包括最后的值。1…< 5 区间值为 1, 2, 3, 和 4

⑦ 其它运算符

  • Swift 提供了其它类型的的运算符,如一元、二元和三元运算符:
    • 一元运算符对单一操作对象操作(如 -a),一元运算符分前置运算符和后置运算符,前置运算符需紧跟在操作对象之前(如 !b),后置运算符需紧跟在操作对象之后(例如 c!)。备注:在 Java/C 没有类似 c!的语法, 在 Swift 中用在 Optional 类型取值;
    • 二元运算符操作两个操作对象(如2 + 3),是中置的,因为它们出现在两个操作对象之间;
    • 三元运算符操作三个操作对象,和 C 语言一样,Swift 只有一个三元运算符,就是三目运算符(a ? b : c)。

二、自定义操作符

① 数字容器

  • 有时定义了实质上只是容器的值类型其容纳着更加原始的值。例如,在一个战略游戏中,玩家可以收集两种资源:木材和金币。要在代码中建模这些资源,可以使用作为木材和金币值的容器的 Resource 结构体,如下所示:
struct Resources {
    var gold: Int
    var wood: Int
}
  • 每当引用一组资源时,就会使用此结构。例如,要跟踪玩家当前可用的资源:
struct Player {
    var resources: Resources
}
  • 可以在游戏中花费资源的一件事是为队伍培训新单位,执行此类动作时,只需从当前的玩家的资源中减去该单元的金币和木材成本:
func trainUnit(ofKind kind: Unit.Kind) {
    let unit = Unit(kind: kind)
    board.add(unit)

    currentPlayer.resources.gold -= kind.cost.gold
    currentPlayer.resources.wood -= kind.cost.wood
}
  • 做到上面的完全有效,但由于游戏中有许多影响玩家资源的动作,代码中有许多地方必须重复金币和木头的两个减法。这不仅使得很容易忘记减少其中一个值,同时它还使得引入一种新的资源类型更难(例如,银币),因为必须通过查看整个代码并更新所有处理资源的地方。

② 操作符重载

  • 现在尝试使用操作符重载来解决上述问题,使用大多数语言(包括 Swift)的操作符时,都有两个选项,重载现有运算符,或者创建一个新的运算符,重载工作就像方法重载,可以使用新的输入或输出创建新版本的操作符。
  • 在这种情况下,将定义 -= 运算符的过载,它们适用于两个 Resources 值,如下所示:
extension Resources {
    static func -=(lhs: inout Resources, rhs: Resources) {
        lhs.gold -= rhs.gold
        lhs.wood -= rhs.wood
    }
}
  • 就像遵守 Equatable 协议的时候一样,Swift 中的操作符重载只是可以在类型上声明的一个正常静态函数。在此处 -= 中,操作符的左侧是一个 inoiut 参数,这是需要修改的值。通过操作符重载,现在可以直接在当前的玩家的资源上简单地调用 -= ,就像将其放在在任何原始数值上:
currentPlayer.resources -= kind.cost
  • 这不仅很好阅读,它还有助于我们消除代码重复问题。由于总是希望所有外部逻辑修改完整的 Resource 实例,因此可以将金币 gold 和木材 wood 属性作为只读属性开放给外部其它类:
struct Resources {
    private(set) var gold: Int
    private(set) var wood: Int

    init(gold: Int, wood: Int) {
        self.gold = gold
        self.wood = wood
    }
}

③ 可变函数

  • 另一种可以解决上面的 Resources 问题的方法是使用可变函数而不是操作符重载,添加一个函数,通过另一个实例减少 Resources 值的属性,如下所示:
extension Resources {
    mutating func reduce(by resources: Resources) {
        gold -= resources.gold
        wood -= resources.wood
    }
}
  • 这两个解决方案都有它们的优点,可以争辩说可变函数方法更明确。但是,我们不希望数学的标准减法 API 变成:5.reduce(by: 3),所以也许这是一个运算符重载表现完美的地方。

④ 布局计算

  • 来看看另一种方案,其中使用操作符重载可能非常好,尽管拥有自动布局和强大的布局 API,但有时发现自己在某些情况下需要进行手动布局计算。
  • 在这样的情况下,它非常常见,必须在二维值上进行数学操作,如 CGPoint,CGSize 和 CGVector。例如,我们可能需要通过使用图像视图的大小和一些额外的边距来计算标签的原点,如下所示:
label.frame.origin = CGPoint(
    x: imageView.bounds.width + 10,
    y: imageView.bounds.height + 20
)
  • 如果可以简单地添加它们,而不是必须始终展开 point 和 size 来使用它们的底层组件,这会不会很好(就像上面对 Resources 的操作一样)?
  • 为了能够这样做,可以通过重载 + 运算符来接受两个 CGSize 实例作为输入,并输出 CGPoint 值:
extension CGSize {
    static func +(lhs: CGSize, rhs: CGSize) -> CGPoint {
        return CGPoint(
            x: lhs.width + rhs.width,
            y: lhs.height + rhs.height
        )
    }
}
  • 通过上面的代码,现在可以写下布局计算:
label.frame.origin = imageView.bounds.size + CGSize(width: 10, height: 20)
  • 这很酷,但必须为我们的位置创造 CGSize 会感到有点奇怪。使这个有点更好的一种方法可以是定义另一个 + 重载,该 + 重载接受包含两个 CGFloat 值的元组,如下所示:
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)

// 或者不写:
label.frame.origin = imageView.bounds.size + (10, 20)
  • 但现在我们正在接近导致操作符的争论出现的核心问题:平衡冗余程度和可读性。由于仍然处理数字,我认为大多数人会发现上面的易于阅读和理解,但随着继续自定义操作符的用途,它变得更加复杂,特别是当我们开始引入全新的操作符时。

⑤ 用于处理错误的自定义运算符

  • 到目前为止,还只是简单的重载了系统已经存在的操作符。但是,如果想开始使用无法真正映射到现有的功能的操作符,我们需要定义自己的。
  • 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
    }
}
  • 做出像上面的唯一主要的缺点是我们直接向我们功能的调用者抛出出任何潜在的错误,需要减少 API 可以抛出的错误量,否则做有意义的错误处理和测试变得非常困难。
  • 理想情况下,我们想要的是给定 API 可以抛出的有限错误,这样就可以轻松地单独处理每种情况。假设我们也想捕获所有潜在的错误,这给了我们最好的两个世界,因此,使用显式 cases 定义一个错误枚举,每个错误的枚举都使用底层错误的关联值,如下所示:
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
    }
}

func perform<T>(_ expression: @autoclosure () throws -> T,
                errorTransform: (Error) -> Error) throws -> T {
    do {
        return try expression()
    } catch {
        throw errorTransform(error)
    }
}
  • 现在就好多了,但是仍然有很多错误转换代码扰乱了实际逻辑,让我们来看看引入一个新的操作符是否能帮助我们稍微清理一下这段代码。

⑥ 添加新的操作符

  • 首先定义我们的新运营商,在这种情况下,将选择 〜> 作为符号(具有替代返回类型的动机,所以正在寻找类似于 ->)的东西。由于这是一个将在两侧工作操作符,因此将其定义为 infix,如下所示:
infix operator ~>
  • 使操作符如此强大的是它们可以自动捕捉它们两侧的上下文,将其与Swift 的 @autoclosure 功能相结合,可以创建一些非常酷的东西。来实现 〜> 作为传递表达式和转换错误的操作符,抛出或返回与原始表达式相同的类型:
func ~><T>(expression: @autoclosure () throws -> T,
           errorTransform: (Error) -> Error) throws -> T {
    do {
        return try expression()
    } catch {
        throw errorTransform(error)
    }
}
  • 那么上述这个操作符能够让我们做什么呢?由于枚举具有关联值的静态函数在 Swift 中也是静态函数,可以简单地在我们的抛出表达式和错误情况之间添加 〜> 操作符,我们希望将任何底层错误转换为如下形式:
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之深入解析如何自定义操作符的主要内容,如果未能解决你的问题,请参考以下文章

Swift之深入解析SwiftUI布局如何自定义AlignmentGuides

Swift之深入解析如何使用Xcode和LLDB v2修改UI元素

Swift之深入解析“结果生成器”的工作原理

Swift之深入解析如何实现Promise

Swift之深入解析如何在Swift中实现状态机

Swift之深入解析枚举enum的底层原理