运算符 -- 重载和定义新的运算符
与Objective-C不同,Swift支持重载操作符这样的特性。最常见的使用方式可能就是定义一些简便的计算了。比如:
struct Vector2D {
var x = 0.0
var y = 0.0
}
func + (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
func - (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x - right.x, y: left.y - right.y)
}
prefix func - (value: Vector2D) -> Vector2D {
return Vector2D(x: -value.x, y: -value.y)
}
postfix func ++ (value: inout Vector2D) -> Vector2D {
value.x += 1;
value.y += 1;
return Vector2D(x: value.x, y: value.y)
}
prefix func ++ (value: inout Vector2D) -> Vector2D {
let rawValue = value
value.x += 1;
value.y += 1;
return Vector2D (x: rawValue.x, y: rawValue.y)
}
上面定义的加号、减号、负号,自增都是已经存在于Swift中的运算符了,我们所做的只是变换它的参数进行重载。如果我们要定义新的操作符的话,需要先对其进行先声明,告诉编译器这个符号其实是一个操作符。
// 定义新的操作符
precedencegroup DotProductPrecedence {
associativity: none
higherThan: MultiplicationPrecedence
}
infix operator +*: DotProductPrecedence
func +*(left: Vector2D, right: Vector2D) -> Double {
return left.x * right.x + left.y * right.y
}
注意 :
- swift 中的操作符不能定义在局部域中,因为至少会希望能在全局范围内使用你的操作符,否则操作符就是去了意义。
- precedencegroup 定义了一个操作符优先级级别。操作符优先级的定义和类型声明有些相似,一个操作符需要属于某个特定的优先级。Swift 标准库中已经定义了一些常用的运算优先级组,比如加法优先级 (
AdditionPrecedence
) 和乘法优先级 (MultiplicationPrecedence
) 等,你可以在这里找到完整的列表。如果没有适合你的运算符的优先级组,你就需要像我们在例子中做得这 样,自己指定结合律方式和优先级顺序了。 - associativity 定义了结合律,即如果多个同类的操作符顺序出现的计算顺序。比如常见的加法和减法都是
left
,就是说多个加法同时出现时按照从左往右的顺序计算 (因为加法满足交换律,所以这个顺序无所谓,但是减法的话计算顺序就很重要了)。点乘的结果是一个Double
,不再会和其他点乘结合使用,所以这里是none
。还有right
。 - higherThan 运算符的优先级,点积运算是优先于乘法运算的。除了
higherThan
,也支持使用lowerThan
来指定优先级低于某个其他组。 - infix 定义的是中位操作符,两边都是输入;
prefix
-- 单目运算符且运算符在操作数前面;postfix
-- 单目运算符且操作数在操作数右边。
Func的参数修饰
Swift 其实是一門讨厌变化的语言。所有有可能的地方,都被默认认为是不可变的,也就是用 let
进行声明的。这样不仅可以确保安全,也能在编译器的性能优化上更有作为。在方法的参数上也是如此,我们不写修饰符的话,默认情况下所有参数都是 let
的。
如果我们要对传入的参数进行修改,我们需要将 let
改为 var
。我们将参数写作 var
后,在 Swift 2.2 中代码是可以通过编译并正确工作的,然而这将触发警告。在输入参数上添加 var
的做法已经被废弃了。现在我们如果想只在函数内部对这样的输入值进行修改的话,只能显式地在函数内部进行使用 var
进行赋值以后再操作了:
func incrementor(variable: Int) -> Int {
var num = variable
num += 1
return num
}
有些时候我们会希望在方法内部直接修改输入的值,这时候我们可以使用 inout
来对参数进行修饰:
func incrementor(variable: inout Int) -> Int {
variable += 1
}
var luckyNumber = 7
incrementor(variable: &luckyNumber)
print(luckyNumber) // 8
因为在函数内部就更改了值,所以也不需要返回了。调用也要改变为相应的形式,在前面加上&
符号。
如果你对 Swift 的 值类型和引用类型 的区别有所了解的话,会知道 Int
其实是一个值类型,我们并不能直接修改它的地址来让它指向新的值。那么这里这种类似 C 中取地址的 &
符号到底做了 额什么? 对于值类型来说,inout
相当于在函数内部创建了一个新的值,然后在函数返回时将这个值赋给 `&` 修饰的变量,这与引用类型的行为是不同的。
最后,要注意的是参数的修饰是具有传递限制的,就是说对于跨越层级的调用,我们需要保证同 一参数的修饰是统一的。比如我们想扩展一下上面的方法,实现一个可以累加任意数 字的 +N器 的话,可以写成这样:
//嵌套的函数,里层和外层的参数的修饰词需保持一致
func makeIncrementor(_ addNumber: Int) -> ((inout Int) -> ()) {
func incrementor(_ variable: inout Int) -> () {
variable += addNumber
}
return incrementor
}
外层的 makeIncrementor
的返回里也需要在参数的类型前面明确指出修饰词,以符合内部的定义, 否则将无法编译通过。
值类型和引用类型
Swift 的类型分为值类型和引用类型两种,值类型在传递和赋值时将进行复制,而引用类型则只会使用引用对象的一个 "指向"。Swift 中的 struct
和 enum
定义的类型是值类型,使用 class
定义的为引用类型。很有意思的是,Swift 中的所有的内建类型都是值类型,不仅包括了传统意义像 Int
,Bool
这些,甚至连 String
,Array
以及 Dictionary
都是值类型的。
使用值类型,一个显而易见的优势就是减少了堆上内存分配和回收的次数。值类型的一个特点是在传递和赋值时进行复制,每次复制 肯定会产生额外开销,但是在 Swift 中这个消耗被控制在了最小范围内,在没有必要复制的时候, 值类型的复制都是不会发生的。也就是说,简单的赋值,参数的传递等等普通操作,虽然我们可能用不同的名字来回设置和传递值类型,但是在内存上它们都是同一块内容。比如下面这样的代码:
func test(_ arr: [Int]) {
for i in arr {
print(i)
}
}
var a = [1,2,3]
var b = a
let c = b
test(a)
这么折腾一圈下来,其实我们只在第一句 a
初始化赋值时发生了内存分配,而之后的 b
,c
甚至传递到 test
方法内的 arr
,和最开始的 a
在物理内存上都是同一个东西。而且这个 a
还只在栈空间上,于是这个过程对于数组来说,只发生了指针移动,而完全没有堆内存的分配和释放的问题,这样的运行效率可以说极高。
值类型被复制的时机是值类型的内容发生改变时,比如下面在 b
中又加入了一个数,此时值复制 就是必须的了:
var a = [1,2,3]
var b = a
b.append(5)
// 此时 a 和 b 的内存地址不在相同
值类型在复制时,会将存储在其中的值类型一并进行复制,而对于其中的引用类型的话,则只复制一份引用。这是合理的行为,因为我们不会希望引用类型莫名其妙地引用到了我们设定以外其他对象:
class MyObject {
var num = 0
}
var myObject = MyObject()
var a = [myObject]
var b = a
b.append(myObject)
myObject.num = 100;
print(b[0].num) //100
print(b[0].num) //100
// myObject 的改动同时影响了 b[0] 和 b[1]
虽然将数组和字典设计为值类型最大的考虑是为了线程安全,但是这样的设计在存储的元素或条目数量较少时,给我们带来了另一个优点,那就是非常高效,因为 "一旦赋值就不太会变化" 这种使用情景在 Cocoa 框架中是占有绝大多数的,这有效减少了内存的分配和回收。但是在少数情况 下,我们显然也可能会在数组或者字典中存储非常多的东西,并且还要对其中的内容进行添加或者删除。在这时,Swift 内建的值类型的容器类型在每次操作时都需要复制一遍,即使是存储的都是引用类型,在复制时我们还是需要存储大量的引用,这个开销就变得不容忽视了。幸好我们还 有 Cocoa 中的引用类型的容器类来对应这种情况,那就是 NSMutableArray
和
NSMutableDictionary
。
所以,在使用数组合字典时的最佳实践应该是,按照具体的数据规模和操作特点来决定到时是使用值类型的容器还是引用类型的容器:在需要处理大量数据并且频繁操作 (增减) 其中元素时,选择NSMutableArray
和
NSMutableDictionary
会更好,而对于容器内条目小而容器本身数目多的情况,应该使用 Swift 语言内建的`Array` 和 `Dictionary` 。