首页 » iOS编程基础:Swift、Xcode和Cocoa入门指南 » iOS编程基础:Swift、Xcode和Cocoa入门指南全文在线阅读

《iOS编程基础:Swift、Xcode和Cocoa入门指南》5.2 运算符

关灯直达底部

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手册。手册还列出了可作为自定义运算符名的特殊字符:


/ = - + ! * % < > & | ^ ? ~  

运算符名还可以包含其他很多符号字符(除了其他字母数字的字符),这些字符更难输入;请参考手册了解正式的列表。