Опционалы в Swift: Optional, nil и безопасная распаковка

Опционалы в Swift: Optional, nil и безопасная распаковка Про IT

Опциональное значение (или опциональное поле) — это значение, которого может не быть. В Swift такое значение описывает тип Optional: переменная типа String? содержит либо строку, либо nil — признак того, что значения нет. Обычная переменная String не может содержать nil (пустая строка "" — это значение, а не его отсутствие), и компилятор не даст использовать String? как String, пока вы явно не распакуете значение.

Ниже — как объявить опционал, пять способов достать из него значение (if let, guard let, ??, optional chaining, !), почему падает программа с ошибкой «Unexpectedly found nil» и что такое неявно распакованный опционал String!. Примеры проверены на Swift 6.3.2 (macOS, 24.09.2026).

Мини-словарь

Термин Что это Пример
Опционал Тип «значение или ничего», полная запись Optional<T>, краткая T? var age: Int?
nil Отсутствие значения у опционала age = nil
Распаковка (unwrapping) Получение значения типа T из T? if let age { ... }
Неявно распакованный опционал T!: опционал, который подставляется как T без явной распаковки var label: UILabel!

Важно не путать: T? и Optional<T> — одно и то же, а T! — не «короткая запись опционала», а отдельный режим использования того же опционала (разбор ниже).

Минимальный пример: опциональное поле в структуре

Типичный случай — поле модели, которое заполнено не у всех: отчество, телефон, дата окончания подписки. Такое поле объявляют с ?:

struct User {
    let name: String
    let middleName: String?   // опциональное поле: отчества может не быть
}

let users = [
    User(name: "Анна", middleName: "Сергеевна"),
    User(name: "Tom", middleName: nil),
]

for user in users {
    // 1. if let: работаем со значением, только если оно есть
    if let middle = user.middleName {
        print("\(user.name): отчество \(middle)")
    } else {
        print("\(user.name): отчества нет")
    }

    // 2. ??: подставляем значение по умолчанию
    let shown = user.middleName ?? "-"
    print("  в анкете: \(shown)")

    // 3. optional chaining: результат тоже опционал
    let letters = user.middleName?.count
    print("  букв: \(String(describing: letters))")
}

Вывод:

Анна: отчество Сергеевна
  в анкете: Сергеевна
  букв: Optional(9)
Tom: отчества нет
  в анкете: -
  букв: nil

Обратите внимание на третью строку каждого блока: user.middleName?.count возвращает не Int, а Int?. Поэтому для Анны напечатано Optional(9), а для Tom — nil, без падения.

Почему нельзя просто сложить Int и Int?

Опционал — отдельный тип, и компилятор проверяет это до запуска. Неверный код:

let base: Int = 100
let bonus: Int? = 20
let total = base + bonus
print(total)

Результат — ошибка компиляции (swiftc 6.3.2):

error: value of optional type 'Int?' must be unwrapped to a value of type 'Int'
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'

Компилятор сам предлагает два пути: ?? (значение по умолчанию) или ! (принудительная распаковка). Исправление, в котором нет риска падения:

let base: Int = 100
let bonus: Int? = 20
let total = base + (bonus ?? 0)
print(total)   // 120
if let bonus {
    print(base + bonus)   // 120
}

Похожая ошибка возникает при var s = nil: компилятор пишет 'nil' requires a contextual type, потому что по одному nil нельзя понять, опционал какого типа нужен. Правильно — указать тип: var s: String? = nil.

Способы распаковки и как выбрать

Способ Синтаксис Если nil Когда брать
Optional binding if let x = opt { } Выполняется else Есть действие для обоих случаев
Ранний выход guard let x = opt else { return } Выход из функции/цикла Без значения дальше работать нельзя
Значение по умолчанию opt ?? значение Берется значение справа Есть осмысленный дефолт
Optional chaining opt?.свойство Вся цепочка дает nil Нужно пройти по цепочке, не распаковывая каждое звено
Принудительная распаковка opt! Программа аварийно завершается Только когда nil означает ошибку программиста

if let и сокращенная форма

if let проверяет опционал и, если значение есть, кладет его в новую константу обычного типа. Эта константа видна только внутри фигурных скобок if.

Начиная со Swift 5.7 можно не повторять имя: if let nickname { ... } — то же, что if let nickname = nickname { ... }. Несколько опционалов проверяются через запятую, и тело выполнится, только если значения есть у всех:

let nickname: String? = "swiftie"
if let nickname {                 // Swift 5.7+: то же, что if let nickname = nickname
    print("Ник: \(nickname.uppercased())")
}

let a: Int? = 3
let b: Int? = nil
if let a, let b {
    print(a + b)
} else {
    print("одного из чисел нет")
}

Выведет Ник: SWIFTIE и одного из чисел нет.

guard let: ранний выход

guard let удобен в функциях: если значения нет, выходим сразу, а распакованная константа остается доступной до конца функции (в отличие от if let). В условие можно добавить проверку диапазона:

func parseAge(_ text: String) -> Int? {
    guard let age = Int(text), (0...150).contains(age) else {
        print("'\(text)': не возраст")
        return nil
    }
    // age здесь уже Int и доступен до конца функции
    return age
}

print(parseAge("42") as Any)
print(parseAge("-5") as Any)
print(parseAge("сорок") as Any)
Optional(42)
'-5': не возраст
nil
'сорок': не возраст
nil

Здесь видно, откуда опционалы берутся в реальном коде: Int("сорок") не бросает исключение, а возвращает nil. Так же ведут себя поиск в словаре (dict["key"]), array.first, try? и многие API Apple.

Оператор ??

opt ?? default возвращает значение опционала или, если там nil, значение справа. Если справа обычное значение, тип результата — уже не опционал; если справа тоже опционал (a ?? b, где оба Int?), результат останется Int?. Дефолт должен быть осмысленным: age ?? 0 превращает «возраст неизвестен» в «ноль лет», и дальше эта подмена может попасть в статистику. Если отличить «нет значения» от нуля важно — используйте if let.

Force unwrap и ошибка «Unexpectedly found nil»

Восклицательный знак после опционала (opt!) говорит компилятору: «здесь точно есть значение». Если его нет, программа падает во время выполнения. Неверный код:

let input = "12a"
let number = Int(input)!
print(number)

Результат запуска (сборка без оптимизаций, режим Debug):

Fatal error: Unexpectedly found nil while unwrapping an Optional value

Процесс завершается с кодом 133 (сигнал SIGTRAP). В сборке с -O (как в Release) программа тоже падает, но в нашем прогоне текст сообщения в консоль не выводился — только аварийное завершение. В iOS-приложении это вылет у пользователя.

Исправление — обработать оба случая:

let input = "12a"
if let number = Int(input) {
    print("Число: \(number)")
} else {
    print("'\(input)' - не целое число")
}

Вывод: '12a' - не целое число.

Прием if x != nil { x! } работает, но проверку и распаковку легко рассинхронизировать при правке; if let делает оба действия сразу. Когда ! уместен: значение гарантировано логикой программы, и его отсутствие — баг, который лучше увидеть сразу (например, ресурс, встроенный в приложение). Даже тогда многие команды предпочитают guard let ... else { fatalError("понятное сообщение") }, чтобы в логе было видно, что именно сломалось.

Неявно распакованный опционал: String!

String! — это опционал Optional<String>, который компилятор разрешает использовать как String без явной распаковки. Проверки при этом нет: если внутри nil, будет то же падение.

let iuo: String! = "hello"
let n: Int = iuo.count          // используется как String без распаковки
let copy = iuo                  // тип выводится как String?, а не String
print(n, type(of: copy))

Вывод: 5 Optional<String>. При var t: String! = nil вызов t.count падает с сообщением Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value.

Где это встречается: свойства, которые заполняются не в инициализаторе, а позже, но гарантированно до использования. Классический пример — @IBOutlet weak var titleLabel: UILabel! в UIKit: связь с элементом интерфейса появляется после загрузки экрана. В собственном коде T! лучше не заводить без такой причины.

Optional — это перечисление

Под капотом Optional<T> — обычный enum с двумя вариантами: .some(значение) и .none (это и есть nil). Поэтому опционал можно разобрать через switch:

switch Int("7") {
case .some(let value): print("есть значение \(value)")
case .none: print("пусто")
}

Вывод: есть значение 7. Это объясняет, почему nil в Swift — не «нулевой указатель», как в C, а значение типа: опционалом может быть и Int, и структура, и класс.

Опциональное поле в JSON: Codable

С опциональными полями чаще всего сталкиваются при разборе ответа сервера. Если поле модели объявлено как String?, JSONDecoder при отсутствии ключа (или значении null) запишет nil, а не упадет:

import Foundation

struct Profile: Codable {
    let login: String
    let phone: String?          // опциональное поле JSON
}
let json = #"[{"login": "anna", "phone": "+7 900 000-00-00"}, {"login": "tom"}]"#
let profiles = try JSONDecoder().decode([Profile].self, from: Data(json.utf8))
for p in profiles {
    print(p.login, p.phone ?? "телефон не указан")
}

let broken = #"[{"phone": "123"}]"#
let result = try? JSONDecoder().decode([Profile].self, from: Data(broken.utf8))
print(result as Any)
anna +7 900 000-00-00
tom телефон не указан
nil

Во втором случае нет обязательного login, декодер бросает ошибку, а try? превращает ее в nil. Граница: try? теряет причину ошибки. Для отладки и логов используйте do { try ... } catch { print(error) }. Еще одно ограничение: String? не спасает, если ключ пришел с другим типом (например, число вместо строки) — это тоже ошибка декодирования.

Выводы

  • Опциональное значение — то, которого может не быть; в Swift это тип Optional<T> (краткая запись T?), отсутствие значения обозначается nil.
  • Компилятор не даст использовать T? как T: сначала распаковка через if let, guard let, ?? или optional chaining.
  • opt! при nil аварийно завершает программу с ошибкой «Unexpectedly found nil while unwrapping an Optional value»; его оставляют для случаев, когда nil означает баг.
  • T! — тот же опционал с автоматической распаковкой, без защиты от падения; типичное место — @IBOutlet.
  • Для опциональных полей в моделях и JSON объявляйте поле T? и решайте, что показывать при nil, явно.

Где применяется / связь с практикой

Освойте тему на практике

Опционалы встречаются в каждом iOS-приложении: ответы сервера с необязательными полями, поиск по словарю, преобразование ввода пользователя, элементы интерфейса. Большая часть вылетов у начинающих — это ! там, где значение на самом деле может отсутствовать. Системно разобрать Swift, UIKit и SwiftUI на учебном проекте можно на курсе по iOS-разработке с нуля. Отдельные темы iOS-разработки в формате живого занятия с преподавателем разбирают на открытых уроках Otus.

FAQ

Чем nil в Swift отличается от null в других языках?
В Swift nil может быть только у опционала, и компилятор требует его обработать. В Kotlin похожая модель (String?), в C# 8+ nullable reference types дают предупреждения, а не ошибки компиляции.

Можно ли сравнить опционал с обычным значением без распаковки?
Да, == работает: Int?(3) == 3 дает true, а опционал, в котором nil, при сравнении с 3 дает false. Для арифметики распаковка все равно нужна.

Что делает ?. при вызове метода, который ничего не возвращает?
Метод выполнится, только если значение есть; результат выражения будет Void?, и по нему можно проверить, был ли вызов: if user?.save() != nil { ... }.

OTUS Журнал