Swift 各个击破——运算符(Operators)

1,090 阅读12分钟

点赞评论,感觉有用的朋友可以关注笔者公众号 iOS 成长指北,持续更新

欢迎与笔者讨论,无论是知识点叙述有误还是笔者论述问题

在本文中,你将学习有关 Swift 编程语言中不同类型运算符的所有知识、它们的语法以及如何使用它们。

撰写本文的想法来自于一个 ~= 引发的认知盲点。

let n = 30
if 10...100 ~= n {
  print("inside!") //inside!
}

大多数编程语言中不存在 ~= 这样的数学运算符,当然更不可能在把它当做判断值是否符合某区间来使用。这就让我们有兴趣去探究一下 Swift 中的运算符。

本文从前人的肩膀和总结中进行梳理,致力于让你一文读懂哪些 Swift 中的运算符

运算符类型

Swift 闭包详解 中我们说明了,在 Swift 中运算符是顶级函数。我们在 运算符和条件语句 中介绍了常规的运算符的使用。可以查看 Define Swift 了解早期 Swift中运算符的伪代码。

一般来说,我们从两个方面来分类运算符,一个是语句中相关项的数量,另一个是运算符的功能。

数量

根据语句中相关项数量我们将运算符分成

  • 一元运算符:该运算符对单个操作数进行操作。

    例如:

    let a = !true
    let num = -5
    
  • 二元运算符:该运算符操作两个操作数。

    例如我们常见的数学运算符,如加减乘除之类的

    let a = 3 + 4
    let b = 5 / 2
    
  • 三元运算符

    该运算符对三个操作数进行运算。三元运算符是一个比较重要的点

    let result = (5 > 10) ? "Value larger" : "Value Smaller"
    

    将三元运算符当做单一条件运算来使用,而不是将其作为嵌套条件运算来使用。

功能

根据运算的功能,我们将运算符大致分成

赋值运算符

在 Swift 中赋值运算符用于为属性(变量/常量)赋值。

运算符描述举例
=简单赋值运算符,将值从右操作数赋给左操作数C = A + B 就是将 A + B 赋值给 C
+=加法和赋值运算符,它将右操作数添加到左操作数,并将结果分配给左操作数C += A 等同于 C = C + A
-=减法和赋值运算符,它从左操作数中减去右操作数,并将结果分配给左操作数C -= A 等同于 C = C - A
*=乘法和赋值运算符,它将右操作数与左操作数相乘,并将结果赋值给左操作数C *= A 等同于 C = C * A
/=除法和赋值运算符,它将左操作数与右操作数除并将结果赋值给左操作数C /= A 等同于 C = C / A
%=趋于和赋值运算符,它使用两个操作数取余并将结果赋值给左操作数C %= A 等同于 C = C % A
<<=左移位和赋值运算符C <<= 2 等同于 C = C << 2
>>=右移位和赋值运算符C >>= 2 等同于 C = C >> 2
&=按位与和赋值运算符C &= 2 等同于 C = C & 2
^=按位异或赋值运算符C ^= 2 等同于 C = C ^ 2
|=按位或运算符和赋值运算符C |= 2 等同于 C = C | 2
var aOperator = 20 //20

aOperator += 20 // 40

aOperator -= 20 // 20

aOperator *= 2 // 40

aOperator /= 2 // 20

aOperator %= 2 // 2

aOperator <<= 2 // 8

aOperator >>= 2 // 2

aOperator &= 1 // 0

aOperator ^= -1 // -1

aOperator |= 1 // -1

在赋值运算符中,我们需要注意的是 Swift 中没有 ++-- 这两个自增自减的运算符。

算术运算符

这些运算符用于执行数学运算,包括乘、除、加、减等。这种运算符属于二元运算符的范畴,它有左右两个操作数。

操作符描述例子(A = 10, B = 20)
+两数相加A + B = 30
两数相减A − B = -10
*两数相乘A * B = 200
/两数相除B / A = 2
%取模运算符,整数或浮点相除后所得的余数B % A = 0

在这里我们以运算符 + 为例,以下面的代码为例,说明 Swift 中二元运算符的函数实现方式

//字符串
func +<T : Strideable>(lhs: T, rhs: T.Stride) -> T
// 集合
func +<C : ExtensibleCollectionType, S : SequenceType where S.Generator.Element == C.Generator.Element>(lhs: C, rhs: S) -> C

我们可以理解二元运算符分成 左算子右算子,所以二元运算符的函数实现两步操作,合并 左算子右算子 然后返回一个新值。

这主要是为了后面我们讲解如何在 Swift 中自定义运算符做准备。

比较运算符

Swift 闭包详解 中我们说明举例关于 < 符号的实现,就是一种比较运算符。

操作符描述例子(A = 10, B = 20)
==检查两个操作数的值是否相等;如果相等,则条件变为 true。(A == B) 值为 false
!=检查两个操作数的值是否不相等;如果相等,则条件变为 true。(A != B) 值为 true
检查左算子是否大于右算子;如果大于,则条件变为 true。(A > B) 值为 false
<检查左算子是否小于右算子;如果小于,则条件变为 true。(A < B) 值为 true
>=检查左算子是否大于等于右算子;如果大于等于,则条件变为 true。(A >= B) 值为 false
<=检查左算子是否小于等于右算子;如果小于等于,则条件变为 true。(A <= B) 值为 true

比较运算符一般用于条件语句中——条件语句处理的是 Bool 值。

=====

Swift 给了我们两个等式运算符,=====,它们做的事情略有不同。

== 是用来判断作用两个操作数是否相等,无论他们使用什么 相等 的基准——这为我们自定义结构体的相等提供了理论基础。

=== 是一个 identity operator,它是用来判断一个类的两个实例是否指向相同的内存。只用引用类型的实例才需要进行这个判断。

一个简单的例子,如下所示

class Person: Equatable {
    var name: String = "iOS成长指北"
    static func == (lhs: Person, rhs: Person) -> Bool {
        lhs.name == rhs.name
    }
}

let person1 = Person()
let person2 = Person()
let person3 = person1

if person1 == person2 {
    print("person1 == person2") // person1 == person2
}
if person1 === person2 {
    print("person1 === person2")
}
if person1 === person3 {
    print("person1","===","person3") // person1 === person3
}

对于基于同一个引用类型对象 Person 生成的两个实例,person1person2,我们定义了一个相等 基准——名字相等就认为这两个对象是相等的。所以person1person2 虽然值相等但是指向两块不同的内存,并不 ===

从某种概念上,将 === 引申意义为恒等,有一定的道理。

===== 在代码实现上,其实是基于两种不同的泛型

func ==<T : Equatable>(lhs: T?, rhs: T?) -> Bool

func ===(lhs: AnyObject?, rhs: AnyObject?) -> Bool

需要注意的是, === 只能比较 AnyObject 类型。当然,你肯定知道这是因为什么。

逻辑运算符

我们经常要处理一些包含多个条件的条件语句,只有满足这些条件的时候,才进行某些操作。

逻辑运算符与布尔(逻辑)值一起使用,并返回布尔值。

操作符描述例子(A 为true, B 为 false)
&&逻辑且,如果两个操作数都不为 false,则条件为 true(A && B) 值为 false
||逻辑或,如果两个条件只要有一个不为 false,则条件为 true(A || B) 值为 true
!逻辑非,用于反转其操作数的逻辑状态,如果值为 true 则反转为 false!(A && B) 值为 true

位运算符

位运算符用于操作数据结构中每个独立的比特位。这是一种偏底层的使用,从语法上来说有些抽象。

它们通常被用在底层开发中,比如图形编程和创建设备驱动。位运算符在处理外部资源的原始数据时也十分有用,比如对自定义通信协议传输的数据进行编码和解码。

pqp&qp|qp^q
00000
01011
11110
10011
操作符描述例子(A 为0011 1100, B 为 0000 1101)
&Bitwise AND Operator(按位与运算符)(A & B) 结果为 0000 1100
|Bitwise OR Operator(按位或运算符)(A | B) 结果为 0011 1101
Bitwise XOR Operator(按位异或运算符)(A ^ B) 结果为 0011 0001
~Bitwise NOT Operator(按位取反运算符)是一元的,具有 翻转 位的效果。(~A ) 结果为 1100 0011
<<Bitwise Left Shift Operators(按位左移运算符)A << 2 结果为 1111 0000
>>Bitwise RightShift Operators(按位右移运算符)A >> 2 结果为 0000 1111

具体代码如下:打印的结果值为 Int,这里我用二进制表示结果

let A = 0b00111100
let B = 0b00001100

var C = A & B // 0000 1100

C = A | B // 0011 1101

C = A ^ B //0011 0001

C = ~A // 1100 0011 

C = A << 2 //1111 0000

C = A >> 2 //0000 1111
枚举和位移操作

关于二级制的问题,我们可以用一个常见的毒药和老鼠的问题来说明我们怎么使用二进制来涵盖绝大多数的场景。

先简述一下这个问题:

有100瓶子的水,只有一瓶是毒药,无法直观上进行辨别,小白鼠服用后立即死亡,问最少需要多少只小白鼠才可以找到毒药?

答案是 7 只小老鼠,因为27 大于 100。

感兴趣的小伙伴们可以在网上查找这道二进制毒药的问题详细解答。这里我们不在赘述。

在 Objective-C 中,大量定义了这种枚举,例如我们常见的 UIView 动画的中的 options 选项。

+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion API_AVAILABLE(ios(7.0));

typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions) {
    UIViewAnimationOptionLayoutSubviews            = 1 <<  0,
    UIViewAnimationOptionAllowUserInteraction      = 1 <<  1, // turn on user interaction while animating
    UIViewAnimationOptionBeginFromCurrentState     = 1 <<  2, // start all views from current value, not initial value
    
    ...

    UIViewAnimationOptionPreferredFramesPerSecondDefault     = 0 << 24,
    UIViewAnimationOptionPreferredFramesPerSecond60          = 3 << 24,
    UIViewAnimationOptionPreferredFramesPerSecond30          = 7 << 24,
    
} API_AVAILABLE(ios(4.0));

但是在 Swift 中我们无法定义这样的枚举,这是因为

fatal error: Raw value for enum case must be a literal

Swift 中定义枚举值的RawValue 需要一个确定的值,而不是经过计算出来的。我们可以使用下述代码实现一个二进制的枚举值

enum Bitwise: UInt8 {
    case one = 0b00000001
    case two = 0b00000010
    case three = 0b00000100
    case four = 0b00001000
    case five = 0b00010000
}

这样子定义虽然可以使用到二进制用来组合条件的优势,但是定义的时候过于麻烦。

open class func animate(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.AnimationOptions = [], animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil)

为了支持组件条件,Swift 中支持组合条件的实现方式就是实现一个数组。

区间运算符

Swift 提供了两种生成范围的方法:..<... 运算符。半开范围运算符 ..< 创建的范围为包含第一个值而不包括最后的值,而闭合范围运算符 ... 创建的范围包括第一个值和最后一个值。

闭合范围(lowerBound...upperBound)
for value in 1...3 {
	print(value) // 1,2,3
}
半开范围(lowerBound..<upperBound)
for value in 1..<3 {
	print(value) // 1,2
}
单边范围(lowerBound... 或者 ...upperBound 或 ..<upperBound)

单边范围是指在一个方向上尽可能连续的范围。可以使用半开范围算子和闭合范围算子来创建它,但是该运算符只能在一侧具有一个值。

单边范围无法使用 for 循环进行处理,我们应该用此来判断是否包含

let range = ..<2

if range.contains(-1) {
    print("iOS 成长指北") //iOS 成长指北
}

nil 合并运算符(Nil-coalescing)

可选 章节中,我们介绍了如何使用 nil 合并运算符 来对可选项进行解包。

在 Objective-C 中,我们经常使用三元运算符来进行值得 nil 处理,如果前值不为nil,返回前值

NSString *result = _result ?:@"哨值"

但是在Swift 中并不能这样子操作,如果使用三元运算符来实现的话,我们需要实现如下

var option: String? = nil
let reslut = option != nil ? option : ""

但是使用nil 合并运算符,可以简化这步操作

let reslut = option ?? "iOS成长指北"

运算符优先级

运算符优先级是用于计算给定数学表达式的规则集合。当在一个表达式中使用多个运算符时,每个部分都将按称为运算符优先级的特定顺序求值。某些运算符的优先级高于其他运算符,这会影响表达式的计算方式。例如,乘法运算符的优先级高于加法运算符。

下面操作符优先级从高到低,只列举部分操作符

运算符Examples结合性
位移操作符优先级>> &<< &>> >>
乘法优先级&* % & * /left
加减优先级` &+ &- + - ^`left
区间运算符..< ...
nil 合并运算符??right
比较运算符优先级!== > < >= <= === ==
逻辑且&&left
逻辑或``left
默认优先级~>
三元运算符优先级?:right
赋值运算符优先级`= %= /= &<<= &>>= &= *= >>= <<= ^=`right

在这些里面,比较引人注目的可能就是 &<<&>>&*&+&- 这些,这些被称为溢出运算符

溢出运算符

当向一个整数类型的常量或者变量赋予超过它容量的值时,Swift 默认会报错,而不是允许生成一个无效的数。这个行为为我们在运算过大或者过小的数时提供了额外的安全性。

然而,当你希望的时候也可以选择让系统在数值溢出的时候采取截断处理,而非报错。Swift 提供了 溢出运算符来让系统支持整数溢出运算。这些运算符都是以 & 开头的。

//UInt8.max 0b11111111
var unsignedOverflow = UInt8.max &+ 1 // 0b00000000
var unsignedOverflow1 = UInt8.max &>> 8 //0b11111111
var unsignedOverflow2 = UInt8.max &<< 8 //0b11111111
var unsignedOverflow3 = UInt8.max >> 8 //0b00000000
var unsignedOverflow4 = UInt8.max << 8 //0b00000000

//UInt8.min 0b00000000
unsignedOverflow = UInt8.min &- 1 //0b11111111
unsignedOverflow1 = 0b00000001 &>> 8 //0b00000001
unsignedOverflow2 = 0b00000001 &<< 8 //0b00000001
unsignedOverflow3 = 0b00000001 >> 8 //0b00000000
unsignedOverflow4 = 0b00000001 << 8 //0b00000000

当超过位数是,&<<&>><<>>都能够保护溢出,但是其便宜大于等于最大位数时,其表现结果是不一样的。

溢出也会发生在有符号整型上。针对有符号整型的所有溢出加法或者减法运算都是按位运算的方式执行的,符号位也需要参与计算

var signedOverflow = Int8.min
signedOverflow = signedOverflow &- 1 // 127

自定义运算符

使运算符如此强大的原因在于,它们可以自动捕获它们两侧的上下文。

除了实现标准运算符,在 Swift 中还可以声明和实现 自定义运算符

我们这里将自定义运算符分成两类,一类是运算符重载,另一类是自定义系统没有的运算符。

运算符重载

继续以我们的购物车为例

struct Customer {
    var shoppingList : [Product] = []
  // 购买
    mutating func buy(_ product: Product) {
        self.shoppingList.append(product)
    }
}

struct Product {

}

我们可以将添加的产品代码重载一个运算符,该运算符允许我们通过使用 += 运算符直接向购物车里添加产品。

extension Customer {
    static func += (lhs: inout Customer, rhs: Product) {
        lhs.add(rhs)
    }
}
customer += product
print(customer.shoppingList)
customer += product1
print(customer.shoppingList)

重载自定义运算符一定注意命名空间的问题,不要全局重载 Swift 已有的操作符。尽量不要影响

不能对默认的赋值运算符 = 进行重载。只有复合赋值运算符可以被重载。同样地,也无法对三元条件运算符 (a ? b : c) 进行重载。

运算符重载的另一个定义就是前面我们说的遵守 Equatable 协议的 == 运算符。

自定义运算符

除了重载一些系统有的运算符以外,我们还可以自定义运算符。

根据运算符出现的位置,我们将运算符分成前缀,中缀和后缀三种运算符。一元运算符存在前缀和后缀之分,对两元运算符来说,你只能定义中缀运算符

自定义前缀/后缀运算符

继续以我们商场的例子来说,我们需要给我们外露一个商品标价,我们可以定义一个运算符来实现这个功能

prefix operator ~>

prefix func ~>(value: NSNumber) -> String {
    let currencyFormatter = NumberFormatter()
    currencyFormatter.numberStyle = .currency
    currencyFormatter.locale = Locale.current

    return currencyFormatter.string(from: value)!
}

let decimalInputPrice: String = ~>843.32 // $843.32

let intInputPrice: String = ~>300 // $300.00

同理,我们定义一个后缀运算符来实现同样的功能

postfix operator ~<

postfix func ~<(value: NSNumber) -> String {
    let currencyFormatter = NumberFormatter()
    currencyFormatter.numberStyle = .currency
    currencyFormatter.locale = Locale.current

    return currencyFormatter.string(from: value)!
}

let decimalInputPrice: String =  843.32~< // $843.32

let intInputPrice: String = 300~< // $300.00
自定义中缀运算符

下面我们用自定义中缀运算符实现一个并集的功能

let firstNumbers:  Set<Int> = [1, 4, 5]
let secondNumbers: Set<Int> = [1, 4, 6]

infix operator +-: AdditionPrecedence
extension Set {
    static func +- (lhs: Set, rhs: Set) -> Set {
        return lhs.union(rhs)
    }
}

let uniqueNumbers = firstNumbers +- secondNumbers
print(uniqueNumbers) // Prints: [1, 4, 5, 6]

你注意到我们定义运算符时加的: AdditionPrecedence 吗?下面我们好好说说这个

运算符优先级

每个自定义运算符都属于某个优先级组。优先级组指定了这个运算符相对于其他中缀运算符的优先级和结合性。

在早期的的 Swift 语言中,我们通过

infix operator =~ { associativity left precedence 130 }

但是在新的 Swift 中,这部分已经废弃了。下面列几个官网的代码

infix operator == : Equal
precedencegroup Equal {
  associativity: left
  higherThan: FatArrow
}

infix operator & : BitAnd
precedencegroup BitAnd {
  associativity: left
  higherThan: Equal
}

infix operator => : FatArrow
precedencegroup FatArrow {
  associativity: right
  higherThan: AssignmentPrecedence
}

我们可以定义我们自定义运算符的优先级以及其左关联和有关联

并不是所有的符号都能够自定义字符串的,你可以通过 运算符 来了解到底哪些可以

彩蛋

在 github 找到了一个有趣的自定义 Emoj 的 + 号运算符的例子

print("🛹" + "❄️") //🏂
print("😬" + "❄️") //🥶
print("😢" + "🔥") //🥵
print("🥕" + "🥬" + "🥒" + "🍅") //🥗
print("🧔" + "💈") //👶🏻
print("🦏" + "🌈") //🦄
print("🔨" + "🔧") //🛠

可以尝试一下这个例子,实现 + 号运算符的重载

感谢你阅读本文! 🚀