$ sudo teach IT

Опционал нельзя просто взять и использовать

В прошлый раз мы выяснили, что опционал — это как коробка, в которой либо лежит значение, либо она пуста (nil). Но с самой коробкой компилятор работать не даст: если функция ждёт обычную строку, а вы протянете ей String?, будет ошибка компиляции. Сначала нужно заглянуть внутрь и убедиться, что там что-то есть, — это и называется разворачиванием опционала (unwrapping). В этом уроке разберём все безопасные способы это сделать, а заодно один небезопасный, которым легко выстрелить себе в ногу.

if let: разворачиваем внутри ветки

Самый частый способ — конструкция if let. Она проверяет опционал и, если внутри есть значение, кладёт его в новую обычную (не опциональную) константу, доступную только внутри блока if:

let input: String? = "42"

if let number = Int(input!) {
    print("Получили число: \(number)")
} else {
    print("Это не число")
}

Здесь важно понимать порядок: сначала Int(...) пытается превратить строку в число и возвращает Int?, а if let number = ... проверяет именно этот результат. Если результат — nil, выполнится ветка else, а переменная number вообще не будет существовать за пределами if. Использовать её после закрывающей фигурной скобки — ошибка компиляции, и это не случайность: гарантировать, что там есть значение, можно только внутри блока, где проверка прошла успешно.

Краткая форма без повторения имени

Когда имя новой константы совпадает с именем опционала, повторять его дважды необязательно. Короткая запись if let name разворачивает опционал name в обычную константу с тем же именем, которая «затеняет» опциональную версию только внутри блока:

var username: String? = "anna"

if let username {
    print("Привет, \(username)!")
}

Это не какое-то отдельное правило, а просто сокращение записи if let username = username — записывать не пришлось только повторяющуюся часть. Работает и с guard let, о которой дальше.

guard let: разворачиваем и обязаны выйти

У if let есть недостаток: развёрнутое значение живёт только внутри блока, а если после проверки нужно написать ещё десять строк кода, они все окажутся вложенными. Для функций есть более удобный инструмент — guard let. Подробно устройство guard как конструкции разберём отдельно, здесь достаточно того, как она применяется к опционалам.

guard let разворачивает опционал и делает новую константу доступной до конца функции, а не только внутри блока. Взамен на это компилятор требует, чтобы ветка else обязательно завершала выполнение — например, через return:

func greeting(for name: String?) -> String {
    guard let name else {
        return "Здравствуйте, гость!"
    }
    return "Здравствуйте, \(name)!"
}

Если бы вы попробовали убрать return из блока else, компилятор отказался бы принять такой код: он не может гарантировать, что дальше по функции name действительно развёрнута, если путь через else не завершает работу. Именно поэтому guard let так хорошо подходит для «ранних проверок» в начале функции — код после неё пишется так, будто опционала никогда и не было.

Оператор ?? — значение по умолчанию вместо ветвления

Часто вся логика на случай nil — это просто взять какое-то значение по умолчанию, и городить ради этого целый if не хочется. Для этого есть оператор ?? (nil-coalescing): он возвращает значение слева, если оно не nil, и значение справа — если nil.

let nickname: String? = nil
let displayName = nickname ?? "гость"
print(displayName)

Здесь напечатается гость, потому что nickname пуст. Результат ?? — уже обычное значение, не опционал, поэтому его можно сразу передавать дальше без всякого разворачивания. Операторы ?? можно выстраивать цепочкой: Swift проверит слева направо и возьмёт первое непустое значение.

let fromServer: String? = nil
let fromCache: String? = nil
let fallback = fromServer ?? fromCache ?? "значение по умолчанию"
print(fallback)

Тут fromServer пуст, поэтому Swift смотрит на fromCache; он тоже пуст, поэтому в итоге fallback получит "значение по умолчанию".

Чем if let отличается от ??

Разница принципиальная, а не косметическая. if let — это ветвление: вы получаете два разных пути выполнения кода, и в каждом можно делать что угодно, хоть печатать разные сообщения, хоть вызывать разные функции. ?? — это выражение, которое просто вычисляет одно значение. Если на случай пустого опционала нужно одно запасное значение и больше ничего — берите ??, это короче. Если на случай nil нужна отдельная логика — печать, выход из функции, другой набор действий, — нужен if let или guard let.

Optional chaining: ?. по цепочке

Опционалы часто вложены друг в друга через точку: у опциональной строки хочется вызвать метод, у результата которого — ещё один. Optional chaining позволяет писать такую цепочку через ?.: если в какой-то момент встретился nil, вся цепочка сразу превращается в nil, без падения программы.

let city: String? = "Ташкент"
let firstLetter = city?.first
print(firstLetter as Any)

Здесь city?.first означает «если city не пуст, возьми у него first, иначе результат — nil». Важная деталь: результат optional chaining всегда сам опционален, даже если исходный метод возвращал бы обычное значение. first у строки и так возвращает Character?, а после ?. тип не меняется — он остаётся опциональным по той же причине, по которой был опциональным до цепочки.

var maybeCity: String? = nil
let letterOrNil = maybeCity?.first
print(letterOrNil ?? "нет города")

Здесь maybeCity пуст, поэтому вся цепочка maybeCity?.first сразу даёт nil, ни один метод по факту не вызывается. Результат optional chaining удобно тут же разворачивать через ?? или if let, как в примерах выше.

Force unwrap: ! и его цена

Есть ещё один способ добраться до значения — принудительное разворачивание через восклицательный знак: someOptional!. Он говорит компилятору: «я точно знаю, что там не nil, доверься мне и просто отдай значение».

let text: String? = "привет"
print(text!.uppercased())

Пока внутри действительно есть значение, всё работает. Но если опционал окажется пустым в момент разворачивания через !, программа не «пропустит» это тихо и не подставит ничего похожего на значение по умолчанию — она аварийно завершится прямо на этой строке, с сообщением об ошибке в консоли. Это называется runtime crash, и для пользователя приложения это выглядит как внезапное закрытие программы.

let missing: String? = nil
// print(missing!) // аварийно завершит программу в момент выполнения этой строки

Из-за этой цены правило простое: ! уместен только тогда, когда невозможность nil видна прямо из кода рядом, а не просто «кажется, что там всегда что-то есть». Например, сразу после того как вы сами присвоили конкретное значение на предыдущей строке. Во всех остальных случаях — а их подавляющее большинство — берите if let, guard let или ??: они разворачивают то же самое значение, но без риска обрушить программу.

Неявно развёрнутый опционал

В старом коде изредка встречается запись вида String! вместо String? — это неявно развёрнутый опционал (implicitly unwrapped optional). По смыслу это всё тот же опционал, который может быть nil, но компилятор позволяет использовать его как обычное значение без явного разворачивания каждый раз — с тем же риском аварийного завершения, если внутри окажется nil. Сегодня такая запись используется редко, в основном в унаследованном коде, поэтому достаточно уметь её узнать, а не применять самостоятельно.

Частые ошибки

  • Пытаться использовать константу из if let за пределами блока if — она существует только внутри него.
  • Забыть return (или другой выход) в ветке else у guard let — компилятор не примет такой код.
  • Путать if let и ??: ?? годится только для простой замены значением по умолчанию, а не когда на случай nil нужна отдельная логика.
  • Ожидать, что результат city?.first — это обычный Character, а не Character?: результат optional chaining всегда опционален.
  • Ставить ! «на всякий случай», лишь бы компилятор перестал ругаться, — если nil действительно возможен, это отложенный крах программы.

Резюме

  • if let разворачивает опционал в новую константу внутри блока if, короткая форма if let name сокращает запись, когда имена совпадают.
  • guard let разворачивает опционал для остатка функции, но требует, чтобы else обязательно завершал выполнение.
  • ?? — это выражение, которое даёт значение по умолчанию, если слева nil; цепочку ?? можно продолжать.
  • if let даёт ветку кода с разной логикой, ?? — просто вычисляет одно значение; выбор зависит от того, нужна ли отдельная логика на случай nil.
  • Optional chaining ?. прерывает цепочку вызовов на первом же nil, а результат всегда остаётся опционалом.
  • ! принудительно разворачивает опционал и аварийно завершает программу, если внутри nil — применяйте только когда это невозможно по смыслу кода.
  • String! — неявно развёрнутый опционал, редкость из старого кода, узнавать нужно, а писать самостоятельно не стоит.

Проверьте себя

4 вопроса

Приветствие по имени

Реализуйте функцию greeting(for name: String?) -> String. Если name не пуст, верните строку "Привет, ИМЯ!", подставив значение из name. Если name — nil, верните строку "Привет, гость!". Используйте безопасное разворачивание опционала — guard let, if let или ??, силовое разворачивание через ! в этой задаче не подойдёт.

Безопасное число из строки

Реализуйте функцию safeNumber(from text: String) -> Int, которая пытается превратить строку в целое число. Если получилось — верните это число. Если строка не является числом (в том числе если это пустая строка) — верните 0. Учтите, что попытка преобразования строки в число сама по себе возвращает опционал, который нужно безопасно развернуть.