5. Optionals
Tony Hoare called the null reference his billion-dollar mistake. Swift’s answer
is to delete it. There is no null, no nil pointer, no undefined, and no value of
type String that is secretly absent. What Swift has instead is a separate type
for “a String, or nothing”, and a compiler that will not let you confuse the
two.
This is the feature you will meet most often and the one that makes Swift feel strict at first and quiet later. A whole category of crash stops being possible, and the price is that you say what should happen when a value is missing, at the place where it might be missing, once.
5.1 The Type of Nothing
Any type T has a partner written T?, which holds either a T or nothing.
The nothing is spelled nil. String? is a different type from String, and
nowhere will one be accepted for the other:
optional_math.swift:3:7: error: value of optional type 'Int?' must be unwrapped to a value of type 'Int'
1 | let a = Int("3")
2 | let b = 4
3 | print(a + b)
| |- note: coalesce using '??' to provide a default when the optional value contains 'nil'
| `- note: force-unwrap using '!' to abort execution if the optional value contains 'nil'
Int("3") returns Int? because Int("banana") has to return something, and
the something is nil. Every function in Swift that can fail to produce a value
says so in its return type: readLine(), array.first, dictionary[key],
Int(someString). You never have to ask the documentation whether a call can
come back empty. The type says.
nil is not zero, not an empty string, and not a pointer. It is one of the two
cases of the type, and it is available for value types as well as objects, so
Int? is as ordinary as String?.
5.2 Getting the Value Out
The main form is if let, which tests and unwraps in one move and gives you the
unwrapped value inside the block:
if let n = Int(text) {
print(n * 2)
} else {
print("not a number")
}
When the new name would repeat the old one, drop the right-hand side:
if let text {
print(text.count)
}
Inside the braces, text is a String rather than a String?, shadowing the
outer name for the length of the block. Several bindings and ordinary conditions
may be chained with commas, checked left to right, and later ones may use the
names bound by earlier ones:
if let a = Int(first), let b = Int(second), a < b {
print(a + b)
}
guard let does the same thing and leaves the name in scope for the rest of the
enclosing block, which is what you want when the missing value means the
function has nothing to do. while let loops until the value is missing, which
is what Chapter 1’s readLine loop was doing.
// Add up the numbers on the input, and say which lines were not numbers.
import Foundation
var total = 0
var bad: [String] = []
while let line = readLine() {
let text = line.trimmingCharacters(in: .whitespaces)
if text.isEmpty {
continue
}
guard let n = Int(text) else {
bad.append(text)
continue
}
total += n
}
print("total \(total)")
if !bad.isEmpty {
print("ignored \(bad.count): \(bad.joined(separator: " "))")
}
$ ./sumlines < sumlines.in
total 37
ignored 1: seven
Three optionals are handled in eighteen lines and none of them is checked
against nil by hand. The loop ends when the input does, the bad line is
collected rather than crashing the program, and n exists only where it means
something.
5.3 Supplying a Default
?? produces the wrapped value if there is one and its right-hand side if there
is not.
// Supplying a value when there is none, and reaching through nothing.
let settings = ["editor": "nano", "shell": "bash"]
let editor = settings["editor"] ?? "vi"
let pager = settings["pager"] ?? "less"
print(editor, pager)
// ?. stops the whole chain the moment anything is missing.
print(settings["editor"]?.uppercased() ?? "unset")
print(settings["pager"]?.uppercased() ?? "unset")
let words = ["alpha", "beta"]
print(words.first?.count ?? 0)
print([String]().first?.count ?? 0)
// ?? chains, and the right side is only evaluated if it is needed.
let names: [String] = []
print(names.first ?? settings["shell"] ?? "nobody")
nano less
NANO
unset
5
0
bash
The right-hand side is not evaluated unless it is needed, so
cache[key] ?? expensiveLookup(key) does the work only on a miss. ?? also
chains, and the result of a ?? b is non-optional when b is, which is what
makes it the normal way to leave optional-land.
5.4 Optional Chaining
?. calls a method or reads a property only if there is something to call it
on, and produces nothing otherwise. The result is optional, because the whole
chain might have produced nothing, and a chain of any length fails as a unit:
a?.b?.c.d is nil the moment any link is nil, and nothing further is
evaluated.
That is the difference between ?. and !. Both reach through an optional;
?. gives up quietly and ! crashes.
5.5 Force Unwrapping
! takes the value out and stops the program if there is not one.
fu.swift:2: Fatal error: Unexpectedly found nil while unwrapping an Optional value
It is the right tool in two situations and the wrong tool everywhere else. The
first is when the surrounding code has already established that the value is
there, as in Chapter 1’s c.wholeNumberValue! under a case "0"..."9". The
second is when a missing value means the program is broken beyond recovery and
crashing immediately, at the line that noticed, is better than limping on: a
resource that is compiled into the program, a constant regular expression that
either parses or the build is wrong.
Everywhere else it is a crash you have chosen to write. When you find yourself
reaching for ! because the compiler is in your way, the compiler is the one
that is right.
try! and as! are the same bargain for errors and for casts, and Chapters 9
and 10 give them the same advice.
5.6 An Optional Is an Enum
None of this is built into the compiler as special syntax. Optional is an
ordinary enum from Chapter 8, with two cases, and the rest is sugar:
enum Optional<Wrapped> {
case none
case some(Wrapped)
}
Which means a switch works on it, and everything a switch can do is
available:
// An Optional is an ordinary enum with two cases, and switches like one.
for text in ["42", "seven", "-1"] {
switch Int(text) {
case .some(let n) where n < 0:
print("\(text) is negative")
case let n?:
print("\(text) is \(n)")
case nil:
print("\(text) is not a number")
}
}
// map applies a function to the value inside and leaves nothing alone.
let maybe = Int("21")
print(maybe.map { $0 * 2 } ?? 0)
print(Int("x").map { $0 * 2 } ?? 0)
// flatMap collapses the optional you would otherwise get twice over.
let ages = ["ada": "36", "grace": "unknown"]
func age(of name: String) -> Int? {
ages[name].flatMap { Int($0) }
}
print(age(of: "ada") ?? -1, age(of: "grace") ?? -1, age(of: "alan") ?? -1)
42 is 42
seven is not a number
-1 is negative
42
0
36 -1 -1
case let n? is shorthand for case .some(let n), and case nil for
case .none. Use the switch when the two branches are both real work and the
if let when one of them is a fallback.
map transforms what is inside without opening it: nothing in, nothing out.
flatMap does the same for a transformation that itself returns an optional,
collapsing the Int?? you would otherwise be holding. age(of:) is the case
this exists for, since the dictionary lookup may fail and the parse may fail
independently, and the caller cares about neither distinction.
Exercise 5-1. Rewrite age(of:) with guard let and no flatMap, then
decide which you would rather read.
Exercise 5-2. Write a function taking [String] and returning [Int]
containing only the entries that parse. Then find the standard library call that
does it in one line.
Exercise 5-3. settings["pager"]?.uppercased() has type String?. What is
the type of settings["pager"]?.count, and what is the type of
(settings["pager"]?.count) ?? 0?
5.7 Implicitly Unwrapped Optionals
var x: Int! declares an optional that unwraps itself whenever it is used,
crashing if it is nil. It exists for values that are genuinely absent for a
short window between construction and first use, which in practice means views
loaded from an interface file on Apple platforms.
Write it when a framework hands you one. Do not introduce one. It is a force unwrap that has been moved away from the line where it will crash, which is the opposite of what you want.
5.8 Habits
An optional means the value can be absent, so an optional whose absent case is impossible is a lie that everyone downstream has to keep handling. If a function always returns a value, say so in its type; the optional is not free caution.
An empty array is not nil and [String]? is almost always a mistake, since
the empty case already says “nothing here” and now there are two ways to say it
and every caller must handle both. The same goes for String? where "" would
do, unless the difference between empty and absent is real and you can say what
it is.
Unwrap early. Take the optionals apart at the edge of your program, where the input arrives, and let everything inside work with values that exist. A codebase that passes optionals into its core is a codebase that checks for nil in fifty places, which is the state Swift was designed to get you out of.
Exercise 5-4. Find three functions in the standard library that return an
optional, and say in one sentence each what the nil means.
Exercise 5-5. Take a program you wrote in another language that checks for null, and work out which of those checks Swift would not require.