Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.