Swift教程之运算符重载

2015-02-03 03:12

这篇文章是本人第一次翻译,难免有错误,翻译的时候使用的是txt,所以格式上面有些不太好。


在早前的IOS 8盛宴系列的教程里,你已经了解到,Swift提供了许多强大的、现代的编程特性,比如泛型、函数式编程、一等类型(first class)的枚举、结构体等特性。

但是现在还有另外一个Swift的特性,你应该知道并且会爱上它,它就是运算符重载。

这是一个很好的方法,你能使用+、-、*、/等操作符作用在你喜欢的任何类型上面。如果你有一定的创造性,你甚至可以定义属于你自己的操作符。

例如:我们在Swift Sprite Kit utility library(https://github.com/raywenderlich/SKTUtils/tree/swift)代码中使用运算符重载去讲多个CGPoints对象相加,例如下面代码:
let pt1 = CGPoint(x: 10, y: 20)
let pt2 = CGPoint(x: -5, y: 0)
let pt3 = pt1 + pt2
let pt4 = pt3 * 100

方便吧?那就马上开始重载吧,增强你的Swift开发的能力吧。


注意:这个Swift的教程是假设你已经具备了基础的Swift开发能力。如果你是新接触Swift,我们建议你先去学习我们的其他的Swift教程(http://www.raywenderlich.com/tutorials#swift).


运算符:概述
注意:这一部分的内容是可选的,如果你想回顾一下运算符及其优先级,还是可以看这部分内容的。如果你已经对这些很熟悉了,可以直接创建一个空的playground,进行下一部分内容:重载(Overloading)。


首先我们创建一个新的playground来帮助你去了解运算符。
添加如下的代码在你的playground中:
var simpleSum = 1 + 3
你能看到我们希望的结果:
4
这里有两个我们熟悉的操作符:
1 首先,你定义了一个叫做simpleSum的变量,并且使用赋值操作符(=)设置了它的值。
2 然后,你使用加操作符(+)计算了两个整数的和。


在这篇教程里,你将像这样重载操作符。但是首先,你需要理解优先级的概念。
优先级
你可能还记得在学校里的数学课上学过的关于操作符的优先级的规则。这些规则使某些操作符比其他得操作符有一个更高的优先级,高优先级的操作符被优先计算。例如乘会在加或者减之前计算。
在你的playground中输入以下的代码,验证在Swift中是否也遵循这些规则。
var sumWithMultiplication = 1 + 3 - 3 * 2
你能看到如下的结果:
-2
当算数操作符有相同的优先级的时候,Swift从左到右去计算这些操作符。在这个例子中,运算符按照如下的顺序计算的:
1.3 * 2:减去(译者注:这个减去可以忽略,主要是为了对应第三步)
2.1 + 3:因为在操作符优先级一样得情况下,优先计算最左边得操作符。
3.4 - 6:这个运算完全依赖于前面高优先级的运算符的运算结果。


注意:如果你想了解Swift中优先级的列表,你能在这里(https://developer.apple.com/library/prerelease/mac/documentation/Swift/Conceptual/Swift_Programming_Language/Expressions.html#//apple_ref/doc/uid/TP40014097-CH32-XID_720)找到完成的运算符优先级的列表。


加不是仅仅能够使用在数字上(Adding Isn’t Just for Ints)
整数运算会按照我们希望的运行,但是你能将+使用到其他的类型上吗?
下面代码证明了,你可以!在你的playground里面添加如下的代码试一试:
var sumArray = [1, 2] + [1, 2]
在这种情况下,Swift将+解释成为append指令。但是如果你是想把每一个位置的元素相加怎么办呢?我们都知道这个叫做向量加法(vector addition)。
当然,你能自己定义一个方法去实现这个功能,在你的playground添加如下的代码再试一试:
func add(left: [Int], right: [Int]) -> [Int] {
   var sum = [Int]() 
   assert(left.count == right.count, "vector of same length only") 
   for (key, v) in enumerate(left) {
       sum.append(left[key] + right[key]) 
   }
   return sum
}
这样你就定义了一个全局的方法,这个方法实现了计算输入的两个数组的相加,首先检测两个输入的数组的长度是否一致,然后将两个数组每一个位置上的元素相加并且存储到一个新的数组里面。
现在添加下面的代码,验证一下你的新方法是否工作正常:
var arr1 = [1, 1]
var arr2 = [1, 1]
var arr3 = add(arr1, arr2)
你将在控制台看到如下的输出:
[2, 2]
它很棒!但是我们必须去调用一个方法去做这件事,为什么我们不可以使用+运算符代替呢?

运算符重载
运算符重载允许你改变现在的作用在特定在的结构体和类上的已经存在的操作符的工作方式(译者注:可能有点乱)。这个不正是你想要的吗--改变+操作符作用在int数组上的方式。
因为运算符重载是作用在playground的全局中的,所以新建一个playground,防止影响你原来写的例子。然后添加如下的代码到你的playground:
func +(left: [Int], right: [Int]) -> [Int] { // 1
   var sum = [Int]() // 2
   assert(left.count == right.count, "vector of same length only")  // 3
   for (key, v) in enumerate(left) {
     sum.append(left[key] + right[key]) // 4
   }
   return sum
}
你已经定义了一个全局的函数,叫做+,它将两个int数组相加然后返回一个int数组。下面分解一下它是怎么工作的:
1.注意这个方法定义没有什么特殊。它是一个普通的方法定义,除了你使用了+作为它的函数名。
2.你创建了一个空的Int数组。
3.这个例子只能工作在两个数组是相同的情况上,所以这里使用assert保证它是这样。
4.然后你枚举了左侧的数组,并且加上了右边的数组在相同位置的值。
在你的playground添加如下的代码,测试一下这个方法:
var sumArray1 = [1, 2, 3] + [1, 2, 3]
最终--你期望的向量相加操作符结果出现了!你将看到如下的结果:
[2, 4, 6]
当然,运算符重载并不都是愉快的。当一个人查看你的代码,他们希望操作符的默认行为,这时候运算符重载会使他们迷惑。虽然这样,但是还是不能阻止你重写+运算符让它去执行数字的减法,当然这样的风险是明显的。
image http://cdn4.raywenderlich.com/wp-content/uploads/2014/09/OperatorRage.png
记住运算符重载的原则:能力越大责任越大(with great power comes great responsibility)。
典型的,当你在一个新的对象上重载运算符的时候,需要保持它原始的语义,而不是定义不同(和让人费解)的行为。
在这个例子中,重载的行为还是保持了原始的语义:向量加法仍然是一种加法。但是当你覆盖了Int数组默认的加行为的时候,过了几个月你可能想要使用Int数组加得默认行为,这个将会使用感到很困惑。
幸运的是Swift让你能够定义属于你自己的自定义的运算符。
定义自定义运算符
这里有三个步骤去定义一个自定义操作符:
1.命名你的运算符
2.选择一种类型
3.设置它的优先级和结合性
定义你的运算符
现在你必须选择一个字符作为你的运算符。自定义运算符可以以/、=、-、+、!、*、%、<、>、&、|、^、~或者Unicode字符开始。这个给了你一个很大的范围去选择你的运算符。但是别太高兴,选择的时候你还必须考虑重复输入的时候更少的键盘键入次数。
在这种情况下,你可以复制粘贴Unicode字符?作为很好适应你例子里面加法的实现。
选择一种类型
在Swift中你能定义一元、二元和三元的操作符。他们表明了运算符操作的数字的数目。
一元操作符与一个操作数相关,比如后置++(i++)或者前置++(++i),他们依赖于运算符与操作数出现的位置。
二元操作符是插入的,因为它出现在两个操作符中间,比如1 + 1。
三元操作符有三个操作数。在Swift中,?:条件操作符是唯一一个三目运算符,比如a?b:c。
你应该基于你的运算符的操作数的个数选择合适得类型。你想要实现两个数组相加,那就定义二元运算符。
设置它的优先级和结合性
由于运算符定义是全局的,所以你要小心的选择你的自定义运算符的优先级和结合性。
这个是十分棘手的,所以有一个比较好的方法,在Swift language reference(https://developer.apple.com/library/mac/documentation/Swift/Conceptual/Swift_Programming_Language/Expressions.html#//apple_ref/doc/uid/TP40014097-CH32-XID_720)中找到一个类似的标准的运算符,然后使用相同的语义。例如,在定义向量加的时候,可以使用与+运算符相同的优先级和结合性。
编写你的自定义运算符
回到你的playground,输入下面代码去定义你的自定义运算符。为了简单,你可能想去复制粘贴?。(译者注:这里可能指的是在使用的过程中去复制这个字符)
infix operator ? { associativity left precedence 140 } // 1
func ?(left: [Int], right: [Int]) -> [Int] { // 2
   var sum = [Int](count: left.count, repeatedValue: 0)
   assert(left.count == right.count, "vector of same length only")
   for (key, v) in enumerate(left) {
       sum[key] = left[key] + right[key]
   }
   return sum
}
这段代码与你前面在第一部分中的重载类似,这段代码主要做了以下几个步骤:
* 定义一个中缀/二元操作符,它有两个操作数并且位于操作符两侧。
* 命名操作符为?。
* 设置结合性为left,表明该操作符在相同优先级时候,将使用操作符的顺序从左到右结合。
* 设置优先级为140,这个是和Int加法有相同的优先级,这些优先级可以在Swift language reference(https://developer.apple.com/library/mac/documentation/Swift/Conceptual/Swift_Programming_Language/Expressions.html#//apple_ref/doc/uid/TP40014097-CH32-XID_720)查看。
在第二部分的代码和你在前面看到的类似,它按照两个数组的顺序将其一个一个的相加。在你的playground中添加下面的代码,测试这个新的运算符:
var sumArray = [1, 2, 3] ? [1, 2, 3]
你将看到和前面重载方法一样的结果,但是这次你有了一个拥有不同语义的操作符。
Bonus Round!
现在你已经知道了怎么去创建一个自定义的运算符,是时候挑战一下你自己了。你已经创建了一个?运算符去执行向量的相加,所以使用现在的知识去创建一个?操作符,使用相似的方法实现两个数组的减法。尽你最大的努力,然后再去查看下面的答案。
infix operator  ? { associativity left precedence 140 }
func ?(left: [Int], right: [Int]) -> [Int] {
 var minus = [Int](count: left.count, repeatedValue: 0)
 assert(left.count == right.count, "vector of same length only")
 for (key, v) in enumerate(left) {
   minus[key] = left[key] - right[key]
 }
 return minus
}
测试:
var subtractionArray = [1, 2, 3] ? [1, 2, 3]


记住相似的操作符
如果你定义了一个新的操作符,不要忘了定义任何相关得运算符。
例如,加等运算符(+=)组合了加和赋值两个运算符成为了一个运算符。由于你的新的运算符语义上是跟加是一样的,一个好的方法是也定义一个加等于运算符。
添加下面得代码到Operator2.playground:
infix operator  ?= { associativity left precedence 140 } // 1
func ?=(inout left: [Int], right: [Int]) { // 2
   left = left ? right
}
第一行是与?运算符一样得声明,它使用了一个组合运算符。
需要注意第二行,声明这个组合运算符的左侧输入参数为inout,这个表示这个参数的值,将会在运算符方法内部直接被修改。作为一个结果,这个运算符不用返回一个值,它直接修改了你的输入值。
在你的playground添加如下的代码,测试这个运算符是否按照你想的方法运行。
你将在控制台看到如下输出:
[3, 5, 7]
现在看看,定义你自己的运算符一点也不难。


为不仅仅是一种类型定义运算符
现在想象你也想为小数定义一个向量加运算符。
一种方式是你按照为Int重载运算符的方式,为Double和Float重载一个新的运算符。它仅仅是几行的代码,但是你必须使用复制/粘贴。如果你像我一样--有代码洁癖--复制代码不是你的第一选择,它会使你的代码很难维护。
使用泛型来解救
幸运的,Swift泛型能帮助你实现这个功能。如果你需要复习一下Swift的泛型,可以找到我们之前发布的文章Swift Generics Tutorial(http://www.raywenderlich.com/82572/swift-generics-tutorial)。
为了有一个干净的上下文环境,我们新建一个playground。添加如下的代码到你的playground中:
infix operator ? { associativity left precedence 140 }
func ?<T>(left: [T], right: [T]) -> [T] { // 1
   var minus = [T]()
   assert(left.count == right.count, "vector of same length only")
   for (key, v) in enumerate(left) {
       minus.append(left[key] + right[key]) // 2
   }
   return minus
}
在第一行,你定义了一个泛型类型得函数?,它有一个类型占位符T。到这里playground不高兴了。你能看到一个编译错误:Could not find an overload for '+' that accepts the supplied arguments.
这个错误来源于第二行,当我们尝试使用+运算符作用在两个类型为T得left和right两个参数上的时候发生错误。Swift并不知道它应该怎么使用+运算符作用在这些参数上,因为它不知道这些参数是什么类型。
扩展一个协议
去掉你的代码,并且用下面的代码代替:
protocol Number {  // 1
    func +(l: Self, r: Self) -> Self // 2
}
 
extension Double : Number {} // 3
extension Float  : Number {}
extension Int    : Number {}
 
infix operator ? { associativity left precedence 140 }
func ?<T: Number>(left: [T], right: [T]) -> [T] { // 4
   var minus = [T]()
   assert(left.count == right.count, "vector of same length only")
   for (key, v) in enumerate(left) {
       minus.append(left[key] + right[key])
   }
   return minus
}
你在这里做了许多的事情,我们回过头来分解一下这些步骤:
1. 你定义了一个协议Number
2. 这个Number定义了一个运算符+
3. 你为Double、Float和Int创建了一个扩展,使它们能够实现Number协议
4. 你使用了一个类型约束去要求T必须实现Number协议
最后,你告诉编译器,T应该怎么去处理+运算符。既然你已经修复了编译错误,那就使用下面得代码分别使用Double数组和Int数组测试一下吧:
var doubleArray = [2.4, 3.6] ? [1.6, 2.4]
var intArray = [2, 4] ? [1, 2]
你将在控制台看到如下输出:
[4.0, 6.0]
[3, 6]
现在这个运算符能够正常在多种数据类型下面工作,并且没有复制代码。如果你想添加更多得数字类型,你只需要简单的生命其实现Number协议就可以了。


在真实得生活中我还能怎么使用运算符重载
难道你就没有想过,如果它没有作用,我会让你浪费这么多的时间在这篇教程上吗?这一部分将要展示给你一个真实得例子,让你了解怎么样在你的项目中更好得使用运算符重载。
运算符和CGPoints
对于这个Demo,你将使用SKTUtils library(https://github.com/raywenderlich/SKTUtils/tree/swift),它是一个方便得Sprite Kit帮助类的集合,当时是为了 iOS Games by Tutorials(http://www.raywenderlich.com/store/ios-games-by-tutorials)这本书的第二版而写的。
你能在github上找到这个框架的仓库。在你命令行界面输入如下的代码,可以Clone一份这个仓库的分支:
git clone https://github.com/raywenderlich/SKTUtils.git --branch swift
你在github上下载下来的是该仓库分支的压缩包zip。
注意:从Xcode6 beta 5开始,在playground中引入你自己的library成为了可能。你需要做的就是将框架和playground绑定在一个workspace中。如果你想知道更多关于这些的内容,请阅读这篇文章Playground has never been so fun(http://corinnekrych.blogspot.fr/2014/08/playground-has-never-been-so-fun.html)。
打开SKUTils/Examples/Playground/SKUTils.xcodeworkspace,并且编译这个项目。
然后从项目导航里面打开MyPlayground.playground。删除现在里面的内容并且添加如下的代码:
import SKTUtils 
 
let pt1 = CGPoint(x: 10, y: 20)
let pt2 = CGPoint(x: -5, y: 0)
let pt3 = pt1 + pt2 
let pt4 = pt3 * 100
你可能很惊讶,你已经在CGPoint上成功的使用+、*运算符,并且编译器并没有出现错误。
{x 10 y 20}
{x -5 y 0}
{x 5 y 20}
{x 500 y 2,000}
这个魔法来自于你在头部引入的SKTUtils。让我们仔细的看一下。


在SKTUtils中的重载
在项目导航中打开SKTUtils/CGPoint+Extension.swift文件。你将看到为CGPoint定义了一个扩展,重载了+和+=运算符。
public func + (left: CGPoint, right: CGPoint) -> CGPoint {
 return CGPoint(x: left.x + right.x, y: left.y + right.y)
}
 
public func += (inout left: CGPoint, right: CGPoint) {
 left = left + right
}
这段代码跟你前面写的类似,只是把访问控制符设置成了public。访问控制符约束着在其他得源文件和模块中能否访问到你的代码。由于SKTUtils是一个框架,所以它需要能够被它自己模块之外访问到,所以定义为了public。
这个戏法解释清楚了,它没有一点魔力,只是一个聪明得编码。
当然,在游戏中CGPoint加法和乘法也是一个很普通得运算,所以在那本书中,重载了CGPoint的运算符简化了代码,使它简洁、易读。我相信你能在你的项目中发现类似的例子。
运算符重载是Swift的一个强大得特性,如果你小心的使用它,它会使你的开发更加高效。


接下来该做什么?
你已经到了这个教程的结尾,我希望你喜欢它!你能在这里(http://cdn5.raywenderlich.com/wp-content/uploads/2014/09/OperatorsPlaygrounds.zip)找到最终的playground。
如果你想学习更多关于运算符重载和Swift的知识,可以查看我们的新书Swift by Tutorials(http://www.raywenderlich.com/store/swift-tutorials-bundle)。
我希望你找到一种方法,在你的项目中使用运算符重载!但是记住:with great power comes great responsibility(能力越大责任越大) ? don’t be Troll Dev! ;]
如果你对于这个教程或者运算符重载有什么问题,可以在下面加入我们的论坛讨论。