swift ?和!之间区别:
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
? 那是什么??,! 原来如此!!
问号和叹号现在的用法都是原来 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)