By using optional chaining, we can get rid of checking nil
optional again and again, but there are also some pitfalls to avoid.
The evaluation of optional chaining might early return a nil
. It means everything we get from an optional chaining will be optional. Consider the code below:
class Toy {
let name: String
init(name: String) {
self.name = name
}
}
class Pet {
var toy: Toy?
}
class Child {
var pet: Pet?
}
If there is a boy called Tom and when we need to know the toy's name which Tom's pet owns, we can get it with the optional chaining:
let toyName = tom.pet?.toy?.name
Although we are accessing the name
property in Toy
, which is defined as a String
type instead of a String?
, we actually get a String?
type constant toyName
. This happens because in the optional chaining, every ?.
would be a nil
, so nil
must be an option for the final evaluated value.
We could apply an optional binding in the result and get the real value if it exists:
if let toyName = tom.pet?.toy?.name {
// Great, Tom has a pet, and the pet happens has a toy.
}
Quite clear? Yes and no. If we only use optional chaining we can always pay attention that the result is an optional, but when we combine it with some other features of Swift, things will get complicated soon. Say we write an extension of Toy
:
extension Toy {
func play() {
//...
}
}
Yeah, now the pet can play the toy:
tom.pet?.toy?.play()
And of course, Bob, Leo and Matt also have their pets. If we abstract the method into a closure, and passing the Child
as a parameter, we would write:
This is wrong code
swift let playClosure = {(child: Child) -> () in child.pet?.toy?.play()}
Simple code, isn't it? But it won't compile at all!
The problem is the type of calling play()
. We do not specify any return type for play()
, which means the method will return a ()
(or we can also write it as Void
, they are the same). Nevertheless, as mentioned above, it is called in an optional chaining. So the result would be an Optional value. Instead of getting the original Void
, we will actually get a ()?
.
let playClosure = {(child: Child) -> ()? in child.pet?.toy?.play()}
Or we should change it to a clearer way, Void?
. Despite the fact that it seems a little weird, it is the truth. When using it, we could check whether the method is called successfully or not by unwrapping the result with optional binding.
if let result: () = playClosure(tom) {
print("Happy~")
} else {
print("No toy :(")
}