Swift Optionals: Schrödinger’s Box
Nothing is a hard problem for computer languages. Let me rephrase: the lack of a value is a hard problem for computer languages. Objective-C nihilistically embraced nothingness and sprinkled its version of nothing. You could set values to nothing (nil
), you could send messages to nil
. It was great. Except when it wasn’t.
First off, nil
wasn’t distinguished from the number form of nothing, 0
. This created some problems. For example, if you were searching for the first instance of a substring in a larger string, the function had to distinguish between the first index of the string and simply not finding the string at all since nil
and 0
were interchangeable. Cocoa gave us NSNotFound
to help in this situation. Except that wasn’t the only situation. C used NULL
, not nil
to represent nothing. Foundation collections like NSArray
and NSDictionary
could only contain objects, so NSNull
was created to be an object representation of nothing.
And then there is Nil
, which is the nothing value for Objective-C classes (as opposed to a class instance).
Also, nil
in Objective-C was also equivalent to the boolean NO
, which was actually pretty handy, but could cause some unexpected behavior sometimes.
So, nil
== 0
== NO
...ish. Clear?
Nothing is complicated. I mean, the lack of a value is complicated. Whatever, you know what I mean.
Introducing Optionals
Swift tries to throw all of the above out the window and it does a pretty decent job of it. In Swift, there is only nil
to represent nothing. All of the above nothing constructs are replaced by nil
with the exception of NSNull
, which it keeps around because there isn’t any other way to insert nothing into an Objective-C collection.
For Swift, nil
is different in that it isn’t equivalent to 0
or NO
. It’s nothing. In a way, this made nil
a lot less useful in Swift. You couldn’t do much with it. You can’t message it. You can’t use it as a boolean or zero. This means that the following code doesn’t work:
// Does not compile.
if variable {
// Do something.
}
If
statements check against booleans in Swift and nil
is not a boolean. It is nothing.
Checking for nil
has to be specific.
if variable == nil {
// Do something
}
Swift is very, very strict about nothing. Swift will only let you set a variable to nil
if it knows that the variable can handle nil
and optionals are the way it does this.
“But I thought you said that nil
was the only ‘nothing’ in Swift!”
I did. Optionals aren’t nothing. Optionals might contain nothing. It’s more the Schrödinger’s Box of constructs. Optional may contain a value, or they may not, but they, themselve are just boxes.
Let’s look at the following code:
var variable: String?
It’s tempting to think about the variable as containing a string. If we are more precise, we could say that it is an “optional string”. But if we are the most precise, the above variable is “an optional that might have a string in it or it might have nothing in it.” Doesn’t exactly roll off of the tongue, but it’s good for this to sink in because, until the optional is unwrapped, it isn’t nil
or a String, it is an optional.
A good illustration of this what happens when we set a text label to an optional.
let value: String? = “Some Value”
print(value)
The object value
is a string here, right? I mean we can see it being set to "Some Value"
right there. So why does does print(value)
give us:
Optional(“Some Value”)
We are printing the optional, not the value wrapped inside the optional. If we wanted to print the value, we have to unwrap it first. In Swift, the most common way to unwrap an optional is to use a conditional statement.
if let unwrappedValue = value {
print(unwrappedValue)
}
This will print out what we expect:Some Value
Kind of wishing that you could go back to the old way of doing things? Don’t worry, not only will you get used to it, when you go back to Objective-C, you’ll feel like it’s the wild west where nothing matters and anything could explode on you at any point.
NSString *str = nil;
NSMutableArray *array = [NSMutableArray array];
[array addObject:str]; /* 🔥 Compiles, but will explode at runtime. */
The above code is simply impossible in a pure Swift environment as the compiler will make sure all variables that may be nil
are wrapped in an optional first before we use it.
Unwrapping Optionals
The main way of unwrapping optionals we saw earlier with the if let
statement. If let
checks to see if there is a non-nil value in the optional and if there is, assigns it to a variable so we can use it. Awesome.
But after a few minutes of unwrapping optionals, you start to get a little tired of if let
. Swift is extremely rigid about nothing. It won’t let you use a value in an optional without unwrapping it first. Fortunately there are devices in the language to help us unwrap.
Unwrapping Multiple Optionals
Let’s say you have to unwrap more than one optional at once. Swift (as of version 1.2) lets us consolidate the unwrapping into one if
statement. Behold:
if let
variable1 = optional1,
variable2 = optional2,
variable3 = optional3 {
// Do something with variable1, variable2, variable3
}
This is a lot more useful than nesting multiple if
statements.
Now let’s say you want to wrap optionals but you want to do something different depending on which optionals contain nil and which optionals contain values. In this situation, the switch
statement comes to our rescue with its powerful pattern matching.
let optiona1: String? = "has value"
let optional2: String? = nil
switch optional1, optional2 {
case (let hasValue, let alsoHasValue):
print("Both have a value!")
case (let hasValue, nil):
print("Only optional 1 has a value")
case (nil, let hasValue):
print("Only optional 2 has a value")
case (nil, nil):
print("Neither optional has a value")
}
Using a switch
has make such a situation a lot less complicated than trying to unwrap with if let
.
Nil-Coalescing Operator (??)
Sometimes we want to use a value in an optional, but if there is no value, we have a default value we’ll use instead. Such as:
let str: String
if let value = optional {
str = value
}else{
str = "Default Value"
}
Seems wordy. We can do better with the ternary operator.
let str = optional != nil ? optional! : "Default Value"
We shortened things down to one line, but there is the little matter of the !
. This works, and there is nothing wrong with it, but live enough time in Swift and that !
starts to make you nervous. Essentially, it puts us back in the world of unsafe code. Even though we just checked the optional and we know that it isn’t nil, there is a better way: the nil-coalescing operator (or, ??
).
let str = optional ?? "Default Value"
This allows us to shorten our code even more, but doesn’t use the !
, so it doesn't make the other Swift developers on your team nervous.
I’ve found the nil-coalescing operator especially useful when assigning optional string values to UI elements such as UILabel.
// optional1: String? = "Label Text"
let label = UILabel()
label.text = optional1
On first glance, this should work. Since the text
property can handle either a nil
or a String
value, it accepts a String?
just fine. That is, until you launch the app and find that your label says Optional(Label Text)
. ☹️
Remember, an optional can contain a nil
or a value, but optional1
, itself, is neither a String
or nil
. It’s an optional, so the label is printing out the optional, not necessarily what’s wrapped inside.
The nil-coalescing operator can come to our rescue.
// optional: String? = "Label Text"
let label = UILabel()
label.text = optional ?? nil
Here, we’re setting the text to the string value contained in the optional and if there isn’t a value, we’re setting the text to nil
, which will clear the label properly.
Wrapping Up Unwrapping Optionals
Optionals can be a little confusing at first and may make you long for the perceived simplicity of nil
/Nil
/NULL
/NSNull
, but stick with it. Optionals in swift are a foundational construct to the point that, with the exception of NSNull
, Swift translates all the several kinds of nothings coming from Cocoa into optionals. Once you get the hang of it, Objective-C feels like walking on a high-wire without a net. 🙈