Swift运算符(如+和>等)并不是语言提供的神奇之物。事实上,它们都是函数;它们是被显式声明和实现的,就像其他函数一样。这也是我在第4章指出的+可以作为reduce调用的最后一个参数进行传递的原因所在;reduce接收一个函数(该函数接收两个参数)并返回一个与第一个参数类型相同的值;+实际上是函数的名字。这还解释了Swift运算符如何针对不同的值类型进行重载的方式。你可以对数字、字符串或数组使用+,每种情况下+的含义都不同,因为名字相同但参数类型不同(签名不同)的两个函数是不同的;根据参数类型,Swift可以确定你调用的是哪个+函数。
这些事实不仅仅是有趣的背后实现细节。它们对于你和代码来说都有实际的含义。你可以重载已有的运算符,并应用到自定义的对象类型上。甚至还可以创建新的运算符!本节将会对此进行介绍。
首先,我们来介绍运算符是如何声明的。显然,要有某种句法形式(这是个计算机科学术语),因为调用运算符函数的方式与通常的函数的方式是不同的。你不会说+(1,2),而是说1+2。即便如此,第2个表达式中的1和2都是+函数调用的参数。那么,Swift是如何知道+函数使用了这种特殊语法呢?
为了探究问题的答案,我们来看看Swift头文件:
infix operator + { associativity left precedence 140}
这是个运算符声明。运算符声明表示这个符号是个运算符,它有多少个参数,关于这些参数存在哪些使用语法。真正重要的地方在于花括号之前的部分:关键字operator,它前面是运算符类型,这里是infix,后跟运算符的名字。类型有:
infix
该运算符接收两个参数,并且运算符位于两个参数中间。
prefix
该运算符接收一个参数,并且运算符位于参数之前。
postfix
该运算符接收一个参数,并且运算符位于参数之后。
运算符也是个函数,因此你还需要一个函数声明,表明参数的类型与函数的结果类型。Swift头文件就是一个示例:
func +(lhs: Int, rhs: Int) -> Int
这是Swift头文件中声明的诸多+函数中的一个。特别地,它是两个参数都是Int的声明。在这种情况下,结果本身就是个Int(局部参数名lhs与rhs并不会影响特殊的调用语法,它表示左侧与右侧)。
运算符声明与相应的函数声明都要位于文件顶部。如果运算符是个prefix或postfix运算符,那么函数声明就必须要以单词prefix或postfix开头;默认是infix,可以省略。
我们可以重写运算符来应用到自定义的对象类型上!下面看个示例,假设有一个装有细菌的瓶子(Vial):
struct Vial { var numberOfBacteria : Int init(_ n:Int) { self.numberOfBacteria = n }}
在将两个Vial合并起来时,你会得到一个由两个Vial中的细菌共同构成的一个Vial。因此,将两个Vial加起来的方式就是将它们中的细菌加到一起:
func +(lhs:Vial, rhs:Vial) -> Vial { let total = lhs.numberOfBacteria + rhs.numberOfBacteria return Vial(total)}
如下代码用于测试新的+运算符重写:
let v1 = Vial(500_000)let v2 = Vial(400_000)let v3 = v1 + v2print(v3.numberOfBacteria) // 900000
对于复合赋值运算符来说,第1个参数是被赋值的一方。因此,要想实现这种运算符,必须要将第1个参数声明为inout。下面为Vial类实现该运算符:
func +=(inout lhs:Vial, rhs:Vial) { let total = lhs.numberOfBacteria + rhs.numberOfBacteria lhs.numberOfBacteria = total}
下面是测试+=重写的代码:
var v1 = Vial(500_000)let v2 = Vial(400_000)v1 += v2print(v1.numberOfBacteria) // 900000
对Vial类重写==比较运算符也是很有必要的。这需要让Vial使用Equatable协议,当然,它不会自动使用Equatable协议,需要我们来实现:
func ==(lhs:Vial, rhs:Vial) -> Bool { return lhs.numberOfBacteria == rhs.numberOfBacteria}extension Vial:Equatable{}
既然Vial是个Equatable,那么它就可以用于indexOf这样的方法上了:
let v1 = Vial(500_000)let v2 = Vial(400_000)let arr = [v1,v2]let ix = arr.indexOf(v1) // Optional wrapping 0
此外,互补的不等运算符!=也会自动应用到Vial上,这是因为它已经根据==运算符定义到所有的Equatable上了。出于同样的原因,如果对Vial重写了<并让其使用Comparable,那么另外3个比较运算符也会自动应用上。
接下来实现一个全新的运算符。作为示例,我向Int注入一个运算符,它会将第1个参数作为底数,将第2个参数作为指数。我将^^作为运算符符号(我本想使用^,不过它已经被占用了)。出于简化的目的,我省略了边际情况的错误检查(如指数小于1等):
infix operator ^^ {}func ^^(lhs:Int, rhs:Int) -> Int { var result = lhs for _ in 1..<rhs {result *= lhs} return result}
代码就是这些!下面来测试一下:
print(2^^2) // 4print(2^^3) // 8print(3^^3) // 27
在定义运算符时,考虑到运算符与其他包含了运算符的表达式之间的关系,你应该指定优先级与结合性规则。我不打算介绍细节,如果感兴趣可以参考Swift手册。手册还列出了可作为自定义运算符名的特殊字符:
/ = - + ! * % < > & | ^ ? ~
运算符名还可以包含其他很多符号字符(除了其他字母数字的字符),这些字符更难输入;请参考手册了解正式的列表。