微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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(){//...}funccallMeWithParam(obj:AnyObject!){//...}letsomeMethod=Selector("callMe")letanotherMethod=Selector("callMeWithParam:")

和 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 错误

这是错误代码

   
    


   
               
privatefunccallMe(){//...}NSTimer.scheduledTimerWithTimeInterval(1,target:self,selector:"callMe",userInfo:nil,repeats:true)

正确的做法是在private前面加上@objc关键字,这样运行时就能找到对应的方法了。

@objcprivatefunccallMe(){//...}NSTimer.scheduledTimerWithTimeInterval(1,repeats:true)

另外,如果方法的第一个参数有外部变量的话,在通过字符串生成Selector时还有一个约定,那就是在方法名和第一个外部参数之间加上with

funcaMethod(externalparamName:AnyObject!){...}

获取对应的获取Selector,应该这么写:

lets=Selector("aMethodWithExternal:")

Sequence

Swift 的for...in可以用在所有实现了SequenceType的类型上,而为了实现SequenceType你首先需要实现一个GeneratorType。比如一个实现了反向的generatorsequence可以这么写:

// 先定义一个实现了 GeneratorType protocol 的类型// GeneratorType 需要指定一个 typealias Element// 以及提供一个返回 Element? 的方法 next()classReverseGenerator:GeneratorType{typealiasElement=Intvarcounter:Elementinit<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{vararray:[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()whileletobj=g.next(){println(obj)}

顺便你可以免费得到的收益是你可以使用像map,filterreduce这些方法,因为它们都有对应SequenceType的版本:

      
           

     
           

      
              
funcmap<S:SequenceType,T>(source:S,transform:(S.Generator.Element)->T)->[T]funcfilter<S:SequenceType>(source:S,includeElement:(S.Generator.Element)->Bool)->[S.Generator.Element]funcreduce<S:SequenceType,U>(sequence:S,initial:U,combine:(U,S.Generator.Element)->U)->U

@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方法时还请特别小心,如果在容易产生歧义或者误解的时候,还是使用完整的闭包写法会比较好。

在 Swift 1.2 中,@autoclosure的位置发生了变化。现在@autoclosure需要像本文中这样,写在参数名的前面作为参数修饰,而不是在类型前面作为类型修饰。但是现在标准库中的方法签名还是写在了接受的类型前面,这应该是标准库中的疏漏。在我们自己实现一个 autoclosure 时,在类型前修饰的写法在 Swift 1.2 中已经无法编译了。

练习

在 Swift 中,其实&&||这两个操作符里也用到了@autoclosure。作为练习,不妨打开 Playground,试试看怎么实现这两个操作符吧?

Optional Chaining

使用 Optional Chaining 可以让我们摆脱很多不必要的判断和取值,但是在使用的时候需要小心陷阱。

因为 Optional Chaining 是随时都可能提前返回nil的,所以使用 Optional Chaining 所得到的东西其实都是 Optional 的。比如有下面的一段代码

classtoy{letname:Stringinit(name:String){self.name=name}}classpet{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对象,如果小朋友有宠物并且宠物有玩具的话,就去玩。于是很可能你会写出这样的代码

letplayClosure={(child:Child)->()inchild.pet?.toy?.play()}

你会发现这么表意清晰的代码居然无法编译!

问题在于对于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 程序设计里可能学过像++这样的“自增”运算符,再加上做了不少关于“判断一个数被各种前置++和后置++折磨后的输出是什么”的考试题,所以之后写代码时也会不自觉地喜欢带上这种风格。于是同样的功能可能会写出类似这样的方法

funcincrementor(variable:Int)->Int{return++variable}

残念..编译错误。为什么在 Swift 里这样都不行呢?答案是因为 Swift 其实是一门讨厌变化的语言。所有有可能的地方,都被认认为是不可变的,也就是用let进行声明的。这样不仅可以确保安全,也能在编译器的性能优化上更有作为。在方法的参数上也是如此,我们不写修饰符的话,认情况下所有参数都是let的,上面的代码等效为:

funcincrementor(letvariable: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(inoutvariable:Int){++variable}

因为在函数内部就更改了值,所以也不需要返回了。调用也要改变为相应的形式,在前面加上&符号:

varluckyNumber=7incrementor(&luckyNumber)println(luckyNumber)// luckyNumber = 8

最后,要注意的是参数的修饰是具有传递限制的,就是说对于跨越层级的调用,我们需要保证同一参数的修饰是统一的。举个例子,比如我们想扩展一下上面的方法,实现一个可以累加任意数字的+N器的话,可以写成这样:

funcmakeIncrementor(addNumber:Int)->((inoutInt)->()){funcincrementor(inoutvariable:Int)->(){variable+=addNumber;}returnincrementor;}

外层的makeIncrementor的返回里也需要在参数的类型前面明确指出修饰词,以符合内部的定义,否则将无法编译通过。

外层的makeIncrementor的返回里也需要在参数的类型前面明确指出修饰词,以符合内部的定义,否则将无法编译通过。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐