本文摘自onevcat文章的一段,原文:http://onevcat.com/2014/06/walk-in-swift/
幽灵一般的 Optional
Swift 引入的最不一样的可能就是 Optional Value 了。在声明时,我们可以通过在类型后面加一个?
来将变量声明为 Optional 的。如果不是 Optional 的变量,那么它就必须有值。而如果没有值的话,我们使用 Optional 并且将它设置为 nil
来表示没有值。
//num 不是一个 Int
var num: Int?
//num 没有值
num = nil //nil
//num 有值
num = 3 //{Some 3}
Apple 在 Session 上告诉我们,Optinal Value 其实就是一个盒子,你盒子里可能装着实际的值,也可能什么都没装。
我们看到 Session 里或者文档里天天说 Optional Optional,但是我们在代码里基本一个 Optional 都没有看到,这是为什么呢?而且,上面代码中给 num
赋值为 3 的时候的那个输出为什么看起来有点奇怪?其实,在声明类型时的这个 ?
仅仅只是 Apple 为了简化写法而提供的一个语法糖。实际上我们是有 Optional 类型的声明,就这里的 num
为例,最正规的写法应该是这样的:
//真 Optional 声明和使用
var num: Optional<Int>
num = Optional<Int>()
num = Optional<Int>(3)
没错,num
不是 Int
类型,它是一个 Optional
类型。到底什么是 Optional
呢,点进去看看:
enum Optional<T> : LogicValue, Reflectable {
case None
case Some(T)
init()
init(_ some: T)
/// Allow use in a Boolean context.
func getLogicValue() -> Bool
/// Haskell's fmap, which was mis-named
func map<U>(f: (T) -> U) -> U?
func getMirror() -> Mirror
}
你也许会大吃一惊。我们每天和 Swift 打交道用的 Optional 居然是一个泛型枚举 enum
,而其实我们在使用这个枚举时,如果没有值,我们就规定这个枚举的是 .None
,如果有,那么它就是Some(value)
(带值枚举这里不展开了,有不明白的话请看文档吧)。而这个枚举又恰好实现了LogicValue
接口,这也就是为什么我们能使用 if
来对一个 Optinal 的值进行判断并进一步进行 unwrap 的依据。
var num: Optional<Int> = 3
if num { //因为有 LogicValue,
//.None 时 getLogicValue() 返回 false
//.Some 时返回 true
var realInt = num!
realInt //3
}
既然 var num: Int? = nil
其实给 num
赋的值是一个枚举的话,那这个 nil
到底又是什么?它被赋值到哪里去了?一直注意的是,Swift 里的 nil 和 objc 里的 nil 完全不是一回事儿。objc 的 nil 是一个实实在在的指针,它指向一个空的对象。而这里的 nil 虽然代表空,但它只是一个语意上的概念,确是有实际的类型的,看看 Swift 的 nil
到底是什么吧:
/// A null sentinel value.
var nil: NilType { get }
nil
其实只是 NilType
的一个变量,而且这个变量是一个 getter。Swift 给了我们一个文档注释,告诉我们 nil
其实只是一个 null 的标记值。实际上我们在声明或者赋值一个 Optional 的变量时,?
语法糖做的事情就是声明一个 Optional<T>
,然后查看等号右边是不是 nil 这个标记值。如果不是,则使用 init(_ some: T)
用等号右边的类型 T 的值生成一个 .Some
枚举并赋值给这个 Optional 变量;如果是 nil,将其赋为 None 枚举。
所以说,Optional背后的故事,其实被这个小小的 ?
隐藏了。
我想,Optional 讨论到这里就差不多了,还有三个小问题需要说明。
首先,NilType
这个类型非常特殊,它似乎是个 built in 的类型,我现在没有拿到关于它的任何资料。我本身逆向是个小白,现在看起来 Swift 的逆向难度也比较大,所以关于 NilType
的一些行为还是只能猜测。而关于 nil
这一 NilType
的类型的变量来说,猜测的话,它可能是Optional.None
的一种类似多型表现,因为首先它确实是指向 0x0 的,并且与 Optional.None 的 content 的内容指向一致。但是具体细节还要等待挖掘或者公布了。
其次,Apple 推荐我们在 unwrap 的时候使用一种所谓的隐式方法,即下面这种方式来 unwrap:
var num: Int? = 3
if let n = num {
//have a num
} else {
//no num
}
最后,这样隐式调用足够安全,性能上似乎应该也做优化(有点忘了..似乎说过),推荐在 unwrap 的时候尽可能写这样的推断,而减少直接进行 unwrap 这种行为。
最后一个问题是 Optional 的变量也可以是 Optinal。因为 Optional 就相当于一个黑盒子,可以知道盒子里有没有东西 (通过 LogicValue),也可以打开这个盒子 (unwrap) 来拿到里面的东西 (你要的类型的变量或者代表没有东西的 nil)。请注意,这里没有任何规则限制一个 Optional 的量不能再次被 Optional,比如下面这种情况是完全 OK 的:
var str: String? = "Hi" //{Some "Hi"}
var anotherStr: String?? = str //{{Some "Hi"}}
这其实是没有多少疑问的,很完美的两层 Optional,使用的时候也一层层解开就好。但是如果是 nil 的话,在这里就有点尴尬...
var str: String? = nil
var anotherStr: String?? = nil
因为我们在 LLDB 里输出的时候,得到了两个 nil
如果说 str
其实是 Optional<String>.None
,输出是 nil 的话还可以理解,但是我们知道 (好吧,如果你认真读了上面的 Optional 的内容的话会知道),anotherStr
其实是Optional<Optional<String>>.Some(Optional<String>.None)
,这是其实一个有效的非空Optional
,至少第一层是。而如果放在 PlayGround 里,anotherStr
得到的输出又是正确的{nil}
。What hanppened? Another Apple bug?
答案是 No,这里不是 bug。为了方便观察,LLDB 会在输出的时候直接帮我们尽可能地做隐式的 unwrap,这也就导致了我们在 LLDB 中输出的值只剩了一个裸的 nil。如果想要看到 Optional 本身的值,可以在 Xcode 的 variable 观察窗口点右键,选中 Show Raw values
,这样就能显示出 None 和 Some 了。或者我们可以直接使用 LLDB 的 fr v -R
命令来打印整个 raw 的值:
可以清楚看到,anotherStr
是 .Some
包了一个 .None
。
(这里有个自动 unwrap 的小疑问,就是写类似 var anotherStr: String? = str
这样的代码也能通过,应该是 ?
语法在这里有个隐式解包,需要进一步确认)
? 那是什么??,! 原来如此!!
问号和叹号现在的用法都是原来 objc 中没有的概念。说起来简单也简单,但是背后也还是不少玄机。原来就已经存在的用法就不说了,这里把新用法从浅入深逐个总结一下吧。
首先是 ?
:
-
?
放在类型后面作为 Optional 类型的标记
这个用法上面已经说过,其实就是一个 Optional<T>
的语法糖,自动将等号后面的内容 wrap 成 Optional。给个用例,不再多说:
var num: Int? = nil //声明一个 Int 的 Optional,并将其设为啥都没有
var str: String? = "Hello" //声明一个 String 的 Optional,并给它一个字符串
-
?
放在某个 Optional 变量后面,表示对这个变量进行判断,并且隐式地 unwrap。比如说:
foo?.somemethod()
相比起一般的先判断再调用,类似这样的判断的好处是一旦判断为 nil
或者说是 false
,语句便不再继续执行,而是直接返回一个 nil。上面的写法等价于
if let maybeFoo = foo {
maybeFoo.somemethod()
}
这种写法更存在价值的地方在于可以链式调用,也就是所谓的 Optional Chaining,这样可以避免一大堆的条件分支,而使代码变得易读简洁。比如:
if let upper = john.residence?.address?.buildingIdentifier()?.uppercaseString {
println("John's uppercase building identifier is \(upper).")
}
注意最后 buildingIdentifier
后面的问号是在 ()
之后的,这代表了这个 Optional 的判断对象是buildingIdentifier()
的返回值。
-
?
放在某个 optional 的 protocol 方法的括号前面,以表示询问是否可以对该方法调用
这中用法相当于以前 objc 中的 -respondsToSelector:
的判断,如果对象响应这个方法的话,则进行调用。例子:
delegate?.questionViewControllerDidGetResult?(self, result)
中的第二个问号。注意和上面在 ()
后的问号不一样,这里是在 ()
之前的,表示对方法的询问。
其实在 Swift 中,默认的 potocol 类型是没有 optional 的方法的,因为基于这个前提,可以对类型安全进行确保。但是 Cocoa 框架中的 protocol 还是有很多 optional 的方法,对于这些可选的接口方法,或者你想要声明一个带有可选方法的接口时,必须要在声明 protocol
时再其前面加上@objc
关键字,并在可选方法前面加上 @optional
:
@objc protocol CounterDataSource {
@optional func optionalMethod() -> Int
func requiredMethod() -> Int
@optional var optionalGetter: Int { get }
}
然后是 !
新用法的总结
-
!
放在 Optional 变量的后面,表示强制的 unwrap 转换:
foo!.somemethod()
这将会使一个 Optional<T>
的量被转换为 T
。但是需要特别注意,如果这个 Optional 的量是 nil 的话,这种转换会在运行时让程序崩溃。所以在直接写 !
转换的时候一定要非常注意,只有在有必死决心和十足把握时才做 !
强转。如果待转换量有可能是 nil 的话,我们最好使用 if let
的语法来做一个判断和隐式转换,保证安全。
-
!
放在类型后面,表示强制的隐式转换。
这种情况下和 ?
放在类型后面的行为比较类似,都是一个类型声明的语法糖。?
声明的是Optional
,而 !
其实声明的是一个 ImplicitlyUnwrappedOptional
类型。首先需要明确的是,这个类型是一个 struct
,其中关键部分是一个 Optional<T>
的 value,和一组从这个 value 里取值的 getter 和 方法:
struct ImplicitlyUnwrappedOptional<T> : LogicValue, Reflectable {
var value: T?
//...
static var None: T! { get }
static func Some(value: T) -> T!
//...
}
从外界来看,其实这和 Optional
的变量是类似的,有 Some
有 None
。其实从本质上来说,ImplicitlyUnwrappedOptional
就是一个存储了 Optional
,实现了 Optional
对外的方法特性的一个类型,唯一不同的是,Optional
需要我们手动进行进行 unwrap (不管是使用 var!
还是let if
赋值,总要我们做点什么),而 ImplicitlyUnwrappedOptional
则会在使用的时候自动地去 unwrap,并对继续之后的操作调用,而不必去增加一次手动的显示/隐式操作。
为什么要这么设计呢?主要是基于 objc 的 Cocoa 框架的两点考虑和妥协。
首先是 objc 中是有指向空对象的指针的,就是我们所习惯的 nil
。在 Swift 中,为了处理和 objc 的 nil 的兼容,我们需要一个可为空的量。而因为 Swift 的目的就是打造一个完全类型安全的语言,因此不仅对于 class,对于其他的类型结构我们也需要类型安全。于是很自然地,我们可以使用 Optional 的空来对 objc 做等效。因为 Cocoa 框架有大量的 API 都会返回 nil,因此我们在用 Swift 表达它们的时候,也需要换成对应的既可以表示存在,也可以表示不存在的 Optional
。
那这样的话,不是直接用 Optional
就好了么?为什么要弄出一个 ImplicitlyUnwrappedOptional
呢?因为易用性。如果全部用 Optional
包装的话,在调用很多 API 时我们就都需要转来转去,十分麻烦。而对于 ImplicitlyUnwrappedOptional
因为编译器为我们进行了很多处理,使得我们在确信返回值或者要传递的值不是空的时候,可以很方便的不需要做任何转换,直接使用。但是对于那些 Cocoa 有可能返回 nil,我们本来就需要检查的方法,我们还是应该写 if 来进行转换和检查。
比如说,以下的写法就会在运行时导致一个 EXC_BAD_INSTRUCTION
let formatter = NSDateFormatter()
let now = formatter.dateFromString("not_valid")
let soon = now.dateByAddingTimeInterval(5.0) // EXC_BAD_INSTRUCTION
因为 dateFromString
返回的是一个 NSDate!
,而我们的输入在原来会导致一个 nil
的返回,这里我们在使用 now 之前需要进行检查:
let formatter = NSDateFormatter()
let now = formatter.dateFromString("not_valid")
if let realNow = now {
realNow.dateByAddingTimeInterval(5.0)
} else {
println("Bad Date")
}
这和以前在 objc 时代做的事情差不多,或者,用更 Swift 的方式做
let formatter = NSDateFormatter()
let now = formatter.dateFromString("not_valid")
let soon = now?.dateByAddingTimeInterval(5.0)
如何写出正确的 Swift 代码
现在距离 Swift 发布已经接近小一周了。很多开发者已经开始尝试用 Swift 写项目。但是不管是作为练习还是作为真正的工程,现在看来大家在写 Swift 时还是带了浓重的 objc 的影子。就如何写出带有 Swift 范儿的代码,在这里给出一点不成熟的小建议。
- 理解 Swift 的类型组织结构。Swift 的基础组织非常漂亮,主要的基础类型大部分使用了
sturct
来完成,然后在之上定义并且实现了各种接口,这样的设计模式其实是值得学习和借鉴的。当然,在实际操作中可能会有很大难度,因为接口比之前灵活许多,可以继承,可以放变量等等,因此在定义接口时如何保持接口的单一性和扩展性是一个不小的考验。 - 善用泛型。很多时候 Swift 的 Generic 并不是显式的,类型推断帮助我们做了很多的事情,因此 Generic 这个概念可能被忽视的比较多。关于泛型这个强大的工具,因为原来 objc 中是没有的,而泛型的一个代表语言 C# 虽然平时有写,但很多时候只是当作类型安全的保证在用,我自己也没有太多心得。但是在日常开发中还是多思考和总结,相信会很有进步。
- 尽快养成符合 Swift 的语法和习惯,比如
if let
,比如对常量习惯性地用let
而不要用var
,在上下文明确的时候省掉原来习惯写的self
,枚举只使用.
,合适地使用_
这样的符号来增加可读性等等。既然写 Swift,就应该入乡随俗,尊重这门语言的规范,这样不管在之后和别人的讨论交流上,还是自我的长期发展上,都会很有帮助。 - 安心等 Apple 进一步完善。现在 Swift 还处在相对很早期的阶段,很多东西虽然已经基本定型了,但是也有不少可塑性。编译器和调试器现在感觉还不太好用(当然,因为还在 beta,也不是说责怪什么),而且对于原来基于 objc 写的 Cocoa 框架还是有很多水土不服的地方。我个人来说,现在的水平使用 Swift 写还凑合 app 这样的级别应该问题不大,在这篇文章之后我暂时不会再进一步深挖 Swift,而是打算等待正式版出来之后再看情况使用。现在 Swift 仅在
String
上可以和 Cocoa 框架完美对接,而对于像Array
这样的类型,虽然通过一些巧妙的方式完成了桥接,但是在实际使用上可能还是需要借助大量的NSArray
,在转换上略显麻烦。按照现在来看,Apple 应该至少会将 Cocoa 框架另外几个重要的类迅速适配 Swift 的语言习惯,如果能找到 一种很方便地使用 Cocoa 框架的方法的话,objc 程序员转型 Swift 就应该相对容易一些了。