Опционалы и безопасная распаковка

Опционал — не «переменная, которая может быть пустой», а обычный enum, и из этого следует почти всё поведение.

Optional<Wrapped> — перечисление с двумя кейсами: .none и .some(Wrapped). Значит, nil в Swift — это не нулевой указатель, а полноценное значение типа.

Как звучит вопрос

«Что такое Optional и как он устроен внутри? Чем if let отличается от guard let? Когда допустим force unwrap?»

Что на самом деле проверяют

Понимаете ли вы, что система типов Swift отделяет «значение отсутствует» от «значение есть», и умеете ли писать код, который не падает в проде. Плюс отдельный маркер зрелости: кандидат, который говорит «force unwrap использовать нельзя никогда», знает правило, но не понимает его смысла.

Устройство изнутри

// примерно так объявлен Optional в стандартной библиотеке
enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

let a: Int? = nil
let b: Optional<Int> = .none
print(a == b)          // true — это один и тот же тип

switch a {
case .some(let value): print("есть \(value)")
case .none:            print("нет значения")
}

Вывод:

true
нет значения

Отсюда следует, что Int? и Int — разные типы, и компилятор не даст сложить их по ошибке. Именно это и есть «безопасность» опционалов, а не какая-то магия рантайма.

Распаковка: if let против guard let

// if let: переменная живёт внутри блока
func describe(_ raw: String?) -> String {
    if let raw, !raw.isEmpty {          // сокращённый синтаксис Swift 5.7+
        return "значение: \(raw)"
    }
    return "пусто"
}

// guard let: early exit, переменная живёт дальше по функции
func loadUser(id: String?) throws -> User {
    guard let id, !id.isEmpty else {
        throw AppError.badInput
    }
    let user = try storage.fetch(id)     // id доступен здесь
    return user
}

Практическое отличие — область видимости и форма кода. guard отсекает невалидные случаи сверху и оставляет «счастливый путь» на нулевом уровне вложенности, поэтому в функциях с 3–4 проверками он читается заметно лучше, чем лесенка из if let. Обязательное требование: ветка else у guard должна выйти из области — return, throw, break, continue или fatalError().

Инструменты вокруг опционалов

let nickname: String? = nil
let title = nickname ?? "аноним"          // nil-coalescing, тип String

struct Profile { var address: Address? }
struct Address { var city: String }

let profile: Profile? = nil
let city = profile?.address?.city         // String? — цепочка снова опционал
let cityOrDash = profile?.address?.city ?? "—"

let number = try? Int("42", format: .number)   // ошибка -> nil
let view = someObject as? UIView                // неудачный каст -> nil

Optional chaining возвращает опционал, даже если само свойство неопционально: результата может не быть, потому что цепочка оборвалась. Это частая точка непонимания на собеседовании.

Двойной опционал

let flags: [String: Int?] = ["retries": 3, "timeout": nil]

let raw = flags["timeout"]        // Int?? — .some(.none)
let flat = flags["timeout"] ?? nil // Int? — уже .none
print(raw == nil, flat == nil)

Вывод:

false true

Внешний слой отвечает на вопрос «есть ли такой ключ», внутренний — «какое значение лежит по ключу». Схлопывают слои через ?? nil или flatMap; на последовательностях ту же роль играет compactMap:

let ids = ["1", "два", "3"]
let numbers = ids.compactMap(Int.init)          // [1, 3], тип [Int]
let nested: Int?? = .some(.some(7))
let flattened: Int? = nested.flatMap { $0 }     // 7

IUO и force unwrap

Тип T! (implicitly unwrapped optional) — это тот же Optional, но компилятор молча распаковывает его при использовании. Он остался в языке ради двух вещей: @IBOutlet, которые заполняются при загрузке из storyboard уже после init, и импортированных Objective-C API без аннотаций nullability. Опасность в том, что падение произойдёт в точке использования, а причина — в том, что кто-то не проинициализировал свойство.

Force unwrap ! честно оправдан там, где nil означает баг программиста, а не состояние данных: ресурс, лежащий в бандле приложения, регулярка с литеральным паттерном, значение сразу после явной проверки. Формулировка для интервьюера: «падаю намеренно и громко, потому что продолжать работу с испорченным инвариантом хуже, чем упасть». А вот данные из сети, ввод пользователя и результаты парсинга форсить нельзя никогда.

Типичные ошибки кандидатов

  • Говорить, что nil — это указатель на ноль, как в C или Objective-C.
  • Считать, что guard let и if let отличаются только «стилем», не упоминая область видимости и early exit.
  • Забывать, что optional chaining возвращает опционал.
  • Путать map и compactMap при разборе массива опционалов.
  • Не знать про двойной опционал в словаре со значением-опционалом.
  • Отвечать «force unwrap запрещён всегда», не отличая баг программиста от состояния данных.

Как ответить кратко

Optional — это enum с кейсами .none и .some(Wrapped), поэтому nil в Swift не указатель, а значение типа, и компилятор не даст перепутать Int? с Int. Распаковываю через if let, когда значение нужно только внутри блока, и через guard let, когда это early exit и переменная нужна дальше по функции. Рядом есть ??, optional chaining, try? и as? — они тоже возвращают опционал. Force unwrap оставляю только там, где nil означает баг программиста: ресурс в бандле, литеральная регулярка. Для данных из сети и от пользователя — только безопасная распаковка.

Проверьте себя
1. Как на самом деле устроен Optional в Swift?
AЭто указатель, который может быть равен нулю, как в Objective-C
BЭто enum с кейсами .none и .some(Wrapped)
CЭто struct с полями value и hasValue, проверяемыми в рантайме
DЭто специальный флаг компилятора, не влияющий на тип
2. Дано: let flags: [String: Int?] = ["timeout": nil]. Какой тип у выражения flags["timeout"]?
AInt
BInt?
CInt??
DОпционал схлопывается автоматически, тип Int?
3. В каком случае force unwrap считается оправданным на собеседовании?
AПри разборе JSON, пришедшего с сервера
BПри чтении текста из поля ввода пользователя
CПри загрузке ресурса, который гарантированно лежит в бандле приложения
DВсегда, если код покрыт тестами