MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift运算符重载与自定义实现

2021-04-307.6k 阅读

一、Swift 运算符基础

在深入探讨运算符重载与自定义实现之前,我们先来回顾一下 Swift 中运算符的基础知识。

1.1 运算符分类

Swift 中的运算符分为多种类型,常见的有算术运算符(如 +-*/)、比较运算符(如 ==!=<>)、逻辑运算符(如 &&||!)、赋值运算符(如 =)等。

算术运算符用于执行基本的数学运算,例如:

let a = 5
let b = 3
let sum = a + b
let difference = a - b
let product = a * b
let quotient = a / b

比较运算符用于比较两个值的大小或相等性,它们返回一个布尔值:

let isEqual = a == b
let isGreater = a > b

逻辑运算符主要用于组合或取反布尔值:

let condition1 = true
let condition2 = false
let combined1 = condition1 && condition2
let combined2 = condition1 || condition2
let negated =!condition1

赋值运算符则用于将一个值赋给一个变量或常量:

var num = 10
num = 20

1.2 运算符优先级与结合性

运算符具有不同的优先级,优先级高的运算符会先被计算。例如,乘法和除法的优先级高于加法和减法。在表达式 3 + 5 * 2 中,5 * 2 会先计算,结果为 10,然后再加上 3,最终结果为 13

结合性决定了相同优先级运算符的计算顺序。有些运算符是左结合的,例如加法和减法,表达式 5 - 3 - 1 会按照从左到右的顺序计算,即 (5 - 3) - 1 = 1。而赋值运算符是右结合的,例如 a = b = c 会被解析为 a = (b = c),先将 c 的值赋给 b,然后再将 b 的值赋给 a

二、运算符重载的概念

2.1 什么是运算符重载

运算符重载允许我们为自定义类型赋予已有的运算符新的含义。在 Swift 中,许多标准库类型已经重载了一些运算符。例如,String 类型重载了 + 运算符,用于字符串拼接:

let str1 = "Hello"
let str2 = " World"
let combinedStr = str1 + str2

这里的 + 运算符对于 String 类型不再是传统的加法运算,而是字符串拼接操作。

2.2 为什么需要运算符重载

当我们定义自己的自定义类型时,为了让这些类型能像标准类型一样方便地使用运算符进行操作,就需要进行运算符重载。比如,我们定义一个表示二维向量的结构体 Vector2D,如果能直接使用 + 运算符来计算两个向量的和,会使代码更加直观和易读。

三、Swift 中运算符重载的实现

3.1 一元运算符重载

一元运算符只作用于一个操作数,例如 +(正号)、-(负号)、!(逻辑非)等。

假设我们有一个表示温度的结构体 Temperature,我们想重载一元负号运算符 -,使其可以得到相反的温度值。

struct Temperature {
    var value: Double
    init(_ value: Double) {
        self.value = value
    }
}

prefix func - (temperature: Temperature) -> Temperature {
    return Temperature(-temperature.value)
}

let normalTemp = Temperature(25.0)
let coldTemp = -normalTemp

在上述代码中,我们使用 prefix 关键字定义了一个前缀一元运算符 -,它接受一个 Temperature 类型的参数,并返回一个新的 Temperature 对象,其温度值是原温度值的相反数。

3.2 二元运算符重载

二元运算符作用于两个操作数,如常见的 +-*/ 等。

以之前提到的 Vector2D 结构体为例,我们来重载 + 运算符,用于计算两个二维向量的和。

struct Vector2D {
    var x: Double
    var y: Double
    init(x: Double, y: Double) {
        self.x = x
        self.y = y
    }
}

func + (lhs: Vector2D, rhs: Vector2D) -> Vector2D {
    return Vector2D(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}

let vector1 = Vector2D(x: 1.0, y: 2.0)
let vector2 = Vector2D(x: 3.0, y: 4.0)
let sumVector = vector1 + vector2

这里我们定义了一个全局函数 +,它接受两个 Vector2D 类型的参数(lhs 表示左边的操作数,rhs 表示右边的操作数),并返回一个新的 Vector2D 对象,其 xy 值分别是两个操作数对应值的和。

3.3 赋值运算符重载

在 Swift 中,赋值运算符 = 不能被重载。但是,复合赋值运算符(如 +=-=*=/= 等)可以被重载。

继续以 Vector2D 结构体为例,我们来重载 += 运算符。

func += (lhs: inout Vector2D, rhs: Vector2D) {
    lhs.x += rhs.x
    lhs.y += rhs.y
}

var vector3 = Vector2D(x: 5.0, y: 6.0)
let vector4 = Vector2D(x: 7.0, y: 8.0)
vector3 += vector4

在这个例子中,+= 运算符的实现函数接受一个 inout 类型的左操作数 lhs(因为我们要修改它的值)和一个普通的右操作数 rhs。函数内部直接修改 lhsxy 值,将其与 rhs 对应的值相加。

3.4 比较运算符重载

比较运算符(如 ==!=<> 等)的重载可以让我们自定义类型支持比较操作。

对于 Vector2D 结构体,我们重载 ==< 运算符。

func == (lhs: Vector2D, rhs: Vector2D) -> Bool {
    return lhs.x == rhs.x && lhs.y == rhs.y
}

func < (lhs: Vector2D, rhs: Vector2D) -> Bool {
    if lhs.x < rhs.x {
        return true
    } else if lhs.x == rhs.x && lhs.y < rhs.y {
        return true
    }
    return false
}

let vector5 = Vector2D(x: 1.0, y: 2.0)
let vector6 = Vector2D(x: 1.0, y: 2.0)
let isEqual = vector5 == vector6
let isLess = vector5 < vector6

== 运算符的重载函数判断两个向量的 xy 值是否都相等,而 < 运算符的重载函数先比较 x 值,如果 x 值相等再比较 y 值,以确定一个向量是否“小于”另一个向量。

四、自定义运算符

4.1 定义自定义运算符

在 Swift 中,我们可以定义自己的运算符。自定义运算符必须以 /=-+!*%&|^~?: 这些字符开头,并且可以包含这些字符的组合。

假设我们想定义一个用于计算向量点积的自定义运算符 ·(这里使用 · 表示点积运算符,实际代码中可以用其他合法字符组合代替)。

infix operator · : MultiplicationPrecedence

func · (lhs: Vector2D, rhs: Vector2D) -> Double {
    return lhs.x * rhs.x + lhs.y * rhs.y
}

let vector7 = Vector2D(x: 3.0, y: 4.0)
let vector8 = Vector2D(x: 5.0, y: 6.0)
let dotProduct = vector7 · vector8

首先,我们使用 infix operator 声明了一个中缀运算符 ·,并指定其优先级为 MultiplicationPrecedence(乘法优先级,这样它在表达式中的计算顺序会和乘法类似)。然后,我们定义了这个运算符的具体实现,它接受两个 Vector2D 类型的操作数,并返回它们的点积值。

4.2 自定义运算符的优先级与结合性

在定义自定义运算符时,可以指定其优先级和结合性。优先级可以使用预定义的优先级组,如 AdditionPrecedence(加法优先级)、MultiplicationPrecedence(乘法优先级)等。结合性可以是 left(左结合)、right(右结合)或 none(无结合性)。

例如,我们定义一个自定义的幂运算符 **,并指定它具有较高的优先级且为右结合。

infix operator ** : MultiplicationPrecedence
postfix operator ** : MultiplicationPrecedence

func ** (lhs: Double, rhs: Double) -> Double {
    return pow(lhs, rhs)
}

func ** (lhs: inout Double, rhs: Double) {
    lhs = pow(lhs, rhs)
}

var num1 = 2.0
let num2 = 3.0
let result1 = num1 ** num2
num1 **= num2

这里我们定义了一个中缀和一个后缀的 ** 运算符。中缀运算符接受两个 Double 类型的操作数并返回幂运算的结果,后缀运算符则是对左操作数进行幂运算并修改其值。由于指定了 MultiplicationPrecedence 优先级,它在表达式中的计算顺序会和乘法类似,并且因为指定为右结合,在连续使用 ** 运算符时会从右向左计算,例如 2 ** 3 ** 2 会被解析为 2 ** (3 ** 2)

五、运算符重载与自定义实现的注意事项

5.1 遵循运算符的常规语义

在进行运算符重载和自定义实现时,尽量遵循运算符的常规语义。例如,重载 + 运算符应该表示某种形式的“相加”或“合并”操作,这样可以使代码对其他开发者来说更易于理解。如果重载的运算符语义与常规语义相差过大,可能会导致代码难以维护和阅读。

5.2 避免运算符滥用

不要过度重载运算符或定义过多不必要的自定义运算符。过多的自定义运算符会使代码变得复杂,增加阅读和理解的难度。只有在确实能够显著提高代码的表达力和简洁性时,才考虑进行运算符重载或自定义。

5.3 注意运算符的优先级和结合性

在定义自定义运算符或重载现有运算符时,要仔细考虑其优先级和结合性。不正确的优先级和结合性设置可能会导致表达式的计算结果与预期不符。如果对优先级和结合性不确定,可以参考标准库中类似运算符的设置,或者通过编写测试代码来验证。

5.4 与其他类型的兼容性

当重载运算符时,要考虑与其他类型的兼容性。例如,如果重载了 + 运算符用于自定义类型与 Int 类型的相加,要确保这种操作在逻辑上是合理的,并且不会导致意外的结果。同时,也要注意不同类型之间的类型转换,以保证运算符的正确使用。

通过深入理解和合理运用运算符重载与自定义实现,我们可以让 Swift 代码更加灵活、直观和高效,为处理自定义类型的数据提供更强大的工具。在实际项目中,根据具体需求谨慎地选择和实现运算符,将有助于提升代码的质量和可维护性。