Swift Tips
转载:转自猫神100个Swift必备Tips,onevcat
Selector
@selector
是 Objective-C 时代的一个关键字,它可以将一个方法转换并赋值给一个SEL
类型,它的表现很类似一个动态的函数指针。在 Objective-C 时 selector 非常常用,从设定 target-action,到自举询问是否响应某个方法,再到指定接受通知时需要调用的方法等等,都是由 selector 来负责的。在 Objective-C 里生成一个 selector 的方法一般是这个样子的:
-(void)callMe{//...}-(void)callMeWithParam:(id)obj{//...}SELsomeMethod=@selector(callMe);SELanotherMethod=@selector(callMeWithParam:);// 或者也可以使用 NSSelectorFromString// SEL someMethod = NSSelectorFromString(@"callMe");// SEL anotherMethod = NSSelectorFromString(@"callMeWithParam:");
一般为了方便,很多人会选择使用@selector
,但是如果要追求灵活的话,可能会更愿意使用NSSelectorFromString
的版本 – 因为我们可以在运行时动态生成字符串,从而通过方法的名字来调用到对应的方法。
在 Swift 中没有@selector
了,我们要生成一个 selector 的话现在只能使用字符串。Swift 里对应原来SEL
的类型是一个叫做Selector
的结构体,它提供了一个接受字符串的初始化方法。像上面的两个例子在 Swift 中等效的写法是:
funccallMe(){//...}funccallMeWithP
aram(obj:AnyObject!){//...}letsomeMethod=Selector("callMe")letanotherMethod=Selector("callMeWithP
aram:")
和 Objective-C 时一样,记得在callMeWithParam
后面加上冒号 (:),这才是完整的方法名字。多个参数的方法名也和原来类似,是这个样子:
functurnByAngle(theAngle:Int,speed:Float){//...}letmethod=Selector("turnByAngle:speed:")
另外,因为Selector
类型实现了StringLiteralConvertible
,因此我们甚至可以不使用它的初始化方法,而直接用一个字符串进行赋值,就可以完成创建了。
最后需要注意的是,selector 其实是 Objective-C runtime 的概念,如果你的 selector 对应的方法只在 Swift 中可见的话 (也就是说它是一个 Swift 中的 private 方法),在调用这个 selector 时你会遇到一个 unrecognized selector 错误:
正确的做法是在private
前面加上@objc
关键字,这样运行时就能找到对应的方法了。
@objcprivatefunccallMe(){//...}NSTimer.scheduledTimerWithTimeInterval(1,repeats:true)
另外,如果方法的第一个参数有外部变量的话,在通过字符串生成Selector
时还有一个约定,那就是在方法名和第一个外部参数之间加上with
:
funcaMethod(externalp
aramName:AnyObject!){...}
想获取对应的获取Selector
,应该这么写:
lets=Selector("aMethodWithExternal:")
Sequence
Swift 的for...in
可以用在所有实现了SequenceType
的类型上,而为了实现SequenceType
你首先需要实现一个GeneratorType
。比如一个实现了反向的generator
和sequence
可以这么写:
// 先定义
一个实现了 GeneratorType protocol 的类型// GeneratorType 需要指定
一个 typealias Element// 以及提供
一个返回 Element? 的
方法 next()classReverseGenerator:GeneratorType{typealiasElement=In
tvarcounter:Elemen
tinit<T>(array:[T]){self.counter=array.count-1}init(start:Int){self.counter=start}funcnext()->Element?{returnself.counter<0?nil:counter--}}// 然后我们来定义 SequenceType// 和 GeneratorType 很类似,不过换成指定
一个 typealias Generator// 以及提供
一个返回 Generator? 的
方法 generate()
structreverseSequence<T>:SequenceType{v
ararray:[T]init(array:[T]){self.array=array}typealiasGenerator=ReverseGeneratorfuncgenerate()->Generator{returnReverseGenerator(array:array)}}letarr=[0,1,2,3,4]// 对 SequenceType 可以使用 for...in 来循环访问foriinReverseSequence(array:arr){println("Index \(i) is \(arr[i])")}
输出为
Index4is4Index3is3Index2is2Index1is1Index0is0
如果我们想要深究for...in
这样的方法到底做了什么的话,如果我们将其展开,大概会是下面这个样子:
varg=array.generate()whilel
etobj=g.next(){println(obj)}
顺便你可以免费得到的收益是你可以使用像map
,filter
和reduce
这些方法,因为它们都有对应SequenceType
的版本:
@autoclosure 和 ??
Apple 为了推广和介绍 Swift,破天荒地为这门语言开设了一个博客(当然我觉着是因为 Swift 坑太多需要一个地方来集中解释)。其中有一篇提到了一个叫做@autoclosure
的关键词。
@autoclosure
可以说是 Apple 的一个非常神奇的创造,因为这更多地是像在 “hack” 这门语言。简单说,@autoclosure
做的事情就是把一句表达式自动地封装成一个闭包 (closure)。这样有时候在语法上看起来就会非常漂亮。
比如我们有一个方法接受一个闭包,当闭包执行的结果为true
的时候进行打印:
funclogIfTrue(predicate:()->Bool){ifpredicate(){println("True")}}
在调用的时候,我们需要写这样的代码
logIfTrue({return2>1})
当然,在 Swift 中对闭包的用法可以进行一些简化,在这种情况下我们可以省略掉return
,写成:
logIfTrue({2>1})
还可以更近一步,因为这个闭包是最后一个参数,所以可以使用尾随闭包 (trailing closure) 的方式把大括号拿出来,然后省略括号,变成:
logIfTrue{2>1}
但是不管那种方式,要么是书写起来十分麻烦,要么是表达上不太清晰,看起来都让人生气。于是@autoclosure
登场了。我们可以改换方法参数,在参数名前面加上@autoclosure
关键字:
funclogIfTrue(@autoclosurepredicate:()->Bool){ifpredicate(){println("True")}}
这时候我们就可以直接写:
logIfTrue(2>1)
来进行调用了,Swift 将会吧2 > 1
这个表达式自动转换为() -> Bool
。这样我们就得到了一个写法简单,表意清楚的式子。
在 Swift 中,有一个非常有用的操作符,可以用来快速地对nil
进行条件判断,那就是??
。这个操作符可以判断输入并在当左侧的值是非nil
的 Optional 值时返回其 value,当左侧是nil
时返回右侧的值,比如:
varlevel:Int?varstartLevel=1varcurrentLevel=level??startLevel
在这个例子中我们没有设置过level
,因此最后startLevel
被赋值给了currentLevel
。如果我们充满好奇心地点进??
的定义,可以看到??
有两种版本:
func??<T>(optional:T?,@autoclosuredefaultValue:()->T?)->T?func??<T>(optional:T?,@autoclosuredefaultValue:()->T)->T
在这里我们的输入满足的是后者,虽然表面上看startLevel
只是一个Int
,但是其实在使用时它被自动封装成了一个() -> Int
,有了这个提示,我们不妨来猜测一下??
的实现吧:
func??<T>(optional:T?,@autoclosuredefaultValue:()->T)->T{switchoptional{case
.some(letvalue):returnvaluecase.None:returndefaultValue()}}
可能你会有疑问,为什么这里要使用autoclosure
,直接接受T
作为参数并返回不行么?这正是autoclosure
的一个最值得称赞的地方。如果我们直接使用T
,那么就意味着在??
操作符真正取值之前,我们就必须准备好一个默认值,这个默认值的准备和计算是会消耗性能的。但是其实要是optional
不是nil
的话,我们是完全不需要这个默认值,而会直接返回optional
解包后的值。这样一来,默认值就白白准备了,这样的开销是完全可以避免的,方法就是将默认值的计算推迟到optional
判定为nil
之后。
就这样,我们可以巧妙地绕过条件判断和强制转换,以很优雅的写法处理对Optional
及默认值的取值了。最后要提一句的是,@autoclosure
并不支持带有输入参数的写法,也就是说只有形如() -> T
的参数才能使用这个特性进行简化。另外因为调用者往往很容易忽视@autoclosure
这个特性,所以在写接受@autoclosure
的方法时还请特别小心,如果在容易产生歧义或者误解的时候,还是使用完整的闭包写法会比较好。
Optional Chaining
使用 Optional Chaining 可以让我们摆脱很多不必要的判断和取值,但是在使用的时候需要小心陷阱。
因为 Optional Chaining 是随时都可能提前返回nil
的,所以使用 Optional Chaining 所得到的东西其实都是 Optional 的。比如有下面的一段代码:
cla
sstoy{letname:Stringinit(name:String){self.name=name}}cla
sspet{vartoy:Toy?}classChild{varpet:Pet?}
在实际使用中,我们想要知道小明的宠物的玩具的名字的时候,可以通过下面的 Optional Chaining 拿到:
lettoyName=xiaoming.pet?.toy?.name
注意虽然我们最后访问的是name
,并且在Toy
的定义中name
是被定义为一个确定的String
而非String?
的,但是我们拿到的toyName
其实还是一个String?
的类型。这是由于在 Optional Chaining 中我们在任意一个?.
的时候都可能遇到nil
而提前返回,这个时候当然就只能拿到nil
了。
在实际的使用中,我们大多数情况下可能更希望使用 Optional Binding 来直接取值的这样的代码:
iflettoyName=xiaoming.pet?.toy?.name{// 太好了,小明既有宠物,而且宠物还正好有个玩具}
可能单独拿出来看会很清楚,但是只要稍微和其他特性结合一下,事情就会变得复杂起来。来看看下面的例子:
extensionToy{funcplay(){//...}}
我们为Toy
定义了一个扩展,以及一个玩玩具的play()
方法。还是拿小明举例子,要是有玩具的话,就玩之:
xiaoming.pet?.toy?.play()
除了小明也许我们还有小红小李小张等等..在这种时候我们会想要把这一串调用抽象出来,做一个闭包方便使用。传入一个Child
对象,如果小朋友有宠物并且宠物有玩具的话,就去玩。于是很可能你会写出这样的代码:
你会发现这么表意清晰的代码居然无法编译!
问题在于对于play()
的调用上。定义的时候我们没有写play()
的返回,这表示这个方法返回Void
(或者写作一对小括号()
,它们是等价的)。但是正如上所说,经过 Optional Chaining 以后我们得到的是一个 Optional 的结果。也就是说,我们最后得到的应该是这样一个 closure:
letplayClosure={(child:Child)->()?inchild.pet?.toy?.play()}
这样调用的返回将是一个()?
(或者写成Void?
会更清楚一些),虽然看起来挺奇怪的,但这就是事实。使用的时候我们可以通过 Optional Binding 来判定方法是否调用成功:
ifletresult:()=playClosure(xiaoming){println("好开心~")}else{println("没有玩具可以玩 :(")}
func 的参数修饰
在声明一个 Swift 的方法的时候,我们一般不去指定参数前面的修饰符,而是直接声明参数:
funcincrementor(variable:Int)->Int{returnvariable+1}
这个方法接受一个Int
的输入,然后通过将这个输入加 1,返回一个新的比输入大 1 的Int
。嘛,就是一个简单的+1器。
有些同学在大学的 C 程序设计里可能学过像++
这样的“自增”运算符,再加上做了不少关于“判断一个数被各种前置++
和后置++
折磨后的输出是什么”的考试题,所以之后写代码时也会不自觉地喜欢带上这种风格。于是同样的功能可能会写出类似这样的方法:
残念..编译错误。为什么在 Swift 里这样都不行呢?答案是因为 Swift 其实是一门讨厌变化的语言。所有有可能的地方,都被默认认为是不可变的,也就是用let
进行声明的。这样不仅可以确保安全,也能在编译器的性能优化上更有作为。在方法的参数上也是如此,我们不写修饰符的话,默认情况下所有参数都是let
的,上面的代码等效为:
funcincrementor(le
tvariable:Int)->Int{return++variable}
let
的参数,不能重新赋值这是理所当然的。要让这个方法正确编译,我们需要做的改动是将let
改为var
:
funcincrementor(varvariable:Int)->Int{return++variable}
现在我们的+1器又可以正确工作了:
varluckyNumber=7letnewNumber=incrementor(luckyNumber)// newNumber = 8println(luckyNumber)// luckyNumber 还是 7
正如上面的例子,我们将参数写作var
后,通过调用返回的值是正确的,而luckyNumber
还是保持了原来的值。这说明var
只是在方法内部作用,而不直接影响输入的值。有些时候我们会希望在方法内部直接修改输入的值,这时候我们可以使用inout
来对参数进行修饰:
funcincrementor(inou
tvariable:Int){++variable}
因为在函数内部就更改了值,所以也不需要返回了。调用也要改变为相应的形式,在前面加上&
符号:
varluckyNumber=7incrementor(&luckyNumber)println(luckyNumber)// luckyNumber = 8
最后,要注意的是参数的修饰是具有传递限制的,就是说对于跨越层级的调用,我们需要保证同一参数的修饰是统一的。举个例子,比如我们想扩展一下上面的方法,实现一个可以累加任意数字的+N器的话,可以写成这样:
funcmakeIncrementor(addNumber:Int)->((inoutInt)->()){funcincrementor(inou
tvariable:Int)->(){variable+=addNumber;}returnincrementor;}
外层的makeIncrementor
的返回里也需要在参数的类型前面明确指出修饰词,以符合内部的定义,否则将无法编译通过。