再议Swift操作符重载

今天我们来谈一谈Swift中的操作 符重载,这一功能非常实用,但是也相当有风险。正所谓“能力越大责任越大”,这句话用来形容操作符重载最合适不过了。它可以令你的代码更加简洁,也可以让 一个函数调用变得又臭又长。而对于那些没怎么读过你的代码的人来说,操作符的使用同时也会让代码的可读性大打折扣。


慎引入,按需使用。比如在连接两个字串的时候你就可以通过重载加法来实现。甚至于你仅在屏幕上输入一个加号,就能响应一个网络链接、播放一段音乐或者完成
你能实现的其他任何功能。然而过于复杂的功能对编码来说简直就是灾难,合理的做法就是仅在必要时重载操作符,不做任何多余的事。如果你非要做点奇怪的事,
那就为网络响应这样的功能取一个明显又合适的函数名。

无论你何时想采用运算符重载,你都需要考虑一下是否值得重载它而不是用一个函数调用。函数调用或许颇费周折,但更能确切的表达它的功能(如果函数名恰到好处的话)。假如你要频繁做类似于加减乘除赋值比较的操作,那么运算符重载的确是个很合适的解决方案。

运算符重载

在Swift里面你应该就像平时使用运算符那样来使用重载算符。虽然仍然有些不足的地方,不过下面的这个例子足够说明重载运算符的使用了。这段代码计算的是两个NSDate对象间的差值,使用了“laterDate–initialDate” 的形式:

func - (left: NSDate, right: NSDate) -> NSDateComponents
{
    let mostUnits: NSCalendarUnit = .YearCalendarUnit | .MonthCalendarUnit | .DayCalendarUnit | .HourCalendarUnit | .MinuteCalendarUnit | .SecondCalendarUnit
    
    let components = NSCalendar.currentCalendar().components(mostUnits, fromDate: right, toDate: left, options:nil)
    
    return components
}

在这个函数中,第一行看上去很多,然而mostUnits表示的不过是我们所关心的日期单元(NSCalendarUnit)的清 单,在这里我选择关注的是年、月、日、时、分、秒。值得注意的是这里并非只有这些日期单元的Flag,可选的还有很多,如果我想知道两个日期之间相隔的周 数我一样可以用别的Flag来替换掉上面这些,使用这些不同Flag可以满足我们对日期计算的大部分需求。往下的一行中,使用了当前时间 NSCalendar.currentCalendar()的components()方法创建了一个NSDateComponents对象,之后在方法 最后返回了这个对象components.

在两个值之间的运算成为中缀运算,运算重载还有后缀运算和前缀运算两种。比如++i和i++分别就是前缀和后缀运算。

如你所见,Swift中所谓重载不过是挂羊头卖狗肉,实际上也只是一个函数调用而已,只不过函数名变成了符号。我们指定了表达式中左右参数的类型,并且还指定了计算结果返回的数据类型必须是一个NSDateComponents对象,下面是代码:

let initialDate = NSDate()
let components = NSDateComponents()
components.year = 1
components.month = 5
components.minute = 42
let laterDate = NSCalendar.currentCalendar().dateByAddingComponents(components, toDate: initialDate, options: nil)!
//Test the overloaded operator
let difference = laterDate - initialDate
//Results
println(difference.year)    //Outputs: "1"
println(difference.month)   //Outputs: "5"
println(difference.day)     //Outputs: "0"
println(difference.hour)    //Outputs: "0"
println(difference.minute)  //Outputs: "42"
println(difference.second)  //Outputs: "0"

这里有一些设置,不过大部分很好理解。我们先 初始化一个日期数据,它的值就是这个NSDate对象的创建日期,另外还有一个NSDateComponents对象包含了它自己的年月和分钟的属性值。 之后使用NSCalendar的dateByAddingComponents方法设置laterDate为1年5个月又42分钟之后的时间。然后测试重 载后的新功能“-”操作符,你会看到最后通过减法输出的结果刚好是我们设置的时间差。

我认为这样的重载还是有点用的,我们可以像平时使用数 学减法那样来计算。然而,这里隐藏某些假定的条件:首先我已经提到过关于NSCalendarUnit的Flag只是使用了一些而非全部;其次,我在重载 函数里明确使用了NSCalendar的当期日期(currentCalendar)在这里没有显示。最后它把计算结果difference的属性值都设 为了0,但我们其实并没有想使用其他的属性。不过就我所知,某些人也许会想到封装所有的属性来解决。

如果使用了普通的函数调用来代替重载运 算符,你可以很清楚使用了哪种格式的日期,选中哪些Flag,以及在diffrence里面可以发现哪些属性被设置过。这就是为什么我说重载运算符只是 “有点用”。原因就是它对代码的读者而言,想要知道函数中究竟隐藏了哪些假定条件的话只能扒开重载运算符的源码看了。

Equatable协议

Swift中的重载运算符里面还是有一些比较不错的地方。你可以通过重载"=="算符来实现对自定义类的比较。假如你想实现这一功能你就需要使用Equatable协议,Equatable协议主要应用于泛型编程(有关Swift的泛型编程可以参考这里)。如果你按照Equatable协议重载了"=="操作符,那么你无需再重载"!="操作符(因为“!=”的重载就是"=="的逻辑否)。

重载"=="的方式与其他的中缀运算的重载方式一致,就像我们在上面所提到的"-"重载一样:

//Globally scoped operator function
func == (left: Temperature, right: Temperature) -> Bool
{
    if left.C == right.C
    {
        return true
    }
    else
    {
        return false
    }
}
//Custom type itself
struct Temperature:Equatable
{
    let C: Double
}
//Test
let tempOne = Temperature(C: 15)
let tempTwo = Temperature(C: 35)
let tempThree = Temperature(C: 15)
let OneTwoEquality = (tempOne == tempTwo)       //Stores:   false
let OneThreeEquality = (tempOne == tempThree)   //Stores:   true

现在我们可以使用“==”重载运算来比较两个Temperature实例了。如果要重载一个算符那么它必须要有一个对应的全局函数,甚至定义在类型之外。

Swift 中关于运算重载还有一个很不错的地方就是Comparable协议。如同遵照Equatable协议一样你在照着Comparable协议实现的时候你需 要重载一个“<”方法。另外如果你重载了"=="和">"运算符之后编译器会为你计算出其它几个运算符"!="、"<"、"& lt;="和">="的判定。可以看这里的一个例子,它同样也是Equatable协议的例子。

总结

本文中所有代码的测试环境均为Xcode 6.0.1。

前面我讨论了一下NSDate的减法,是因为它在被调用时隐藏了不少的假定条件。在这里提醒一下:当你在考虑编码中是否要实现重载运算符的时候可以想想这个示例。如果你想在代码里一直使用相同的unitFlag来重载实现日期的减法,你需要多写一些代码来考虑多种情况。

如果你想重载内建类型以及不想修改(或者无法修改)的类型的运算符重载,你可以在扩展里面添加这个重载函数。且这个重载函数需要为全局作用域,并放在你随便哪个扩展存放的文件里面。

有一些运算符是不能重载的,最为明显的就是赋值运算"="(一个等号)。就算你重载了,想一想赋值运算的使用频率,我不会对产生的错误有任何兴趣的。这里有Tammo Freese同学的一篇文章,你可以了解一下哪些运算符是不可以被重载的。

上一篇:bnuoj 4207 台风(模拟题)


下一篇:1012 The Best Rank (25分) vector与结构体排序