Урок по треку Swift и iOS 6 мин

Опционалы в Swift без крэшей

Optional как enum, if let и guard let, ??, опциональные цепочки, force unwrap и IUO.

Опционал это способ сказать «значения может не быть» так, чтобы компилятор не дал об этом забыть. Большая часть крэшей в iOS-приложениях исторически была про nil, и Swift решил эту проблему на уровне типов.

Что такое Optional

String? это сахар для перечисления:

enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

nil это литерал для .none. Никаких нулевых указателей: опционал это обычный тип значения. Поэтому его можно разбирать через switch:

switch user {
case .some(let u): print(u.name)
case .none: print("гость")
}

Безопасная распаковка

if let: значение доступно внутри блока.

if let name = user?.name {
    greet(name)
}
// с Swift 5.7 короче:
if let user { greet(user.name) }

guard let: ранний выход, значение доступно до конца функции. Блок else обязан покинуть область (return, throw, break, continue).

func show(_ id: String?) {
    guard let id, let number = Int(id) else { return }
    print(number)
}

??: значение по умолчанию. Правая часть вычисляется лениво (это @autoclosure).

let title = item.title ?? "Без названия"

Опциональная цепочка ?.: если любое звено nil, результат nil. Уровни не накапливаются: user?.address?.city.count имеет тип Int?, а не Int??.

Преобразования без распаковки

let length = name.map { $0.count }          // Int?
let number = text.flatMap { Int($0) }        // Int?, а не Int??
let ids = ["1", "x", "3"].compactMap(Int.init) // [1, 3]

map у Optional применяет функцию к содержимому, flatMap нужен, когда функция сама возвращает опционал. У массивов compactMap отбрасывает nil.

Проваливаемые инициализаторы

init? может вернуть nil: Int("12a"), URL(string:), UIImage(named:), Enum(rawValue:). Свой:

struct Email {
    let value: String
    init?(_ raw: String) {
        guard raw.contains("@") else { return nil }
        value = raw
    }
}

Принудительная распаковка

x! при nil вызывает аварийное завершение «Unexpectedly found nil». Implicitly unwrapped optional (String!) распаковывается так же неявно при каждом обращении. Исторически IUO нужен для @IBOutlet: аутлет заполняется после загрузки view, а не в init.

Когда ! допустим: если nil означает ошибку программиста, а не данных. Например, URL из строкового литерала или ресурс из бандла. Даже там лучше написать понятное сообщение:

guard let url = Bundle.main.url(forResource: "config", withExtension: "json") else {
    preconditionFailure("config.json не добавлен в таргет")
}

Данные из сети, от пользователя или с диска распаковываются только безопасно.

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

Они появляются, когда опционал лежит внутри другого:

let dict: [String: Int?] = ["a": 1, "b": nil]
dict["b"]        // Int??: ключ есть, значение nil
dict["b"] == nil // false

Присваивание dict["b"] = nil удаляет ключ, а чтобы записать именно «значение nil», нужно .some(nil). На практике опционалы в словарях лучше не хранить.

С Swift 5 try? не добавляет лишний уровень: try? f() для f() throws -> Int? даёт Int?.

weak и опциональность

weak var delegate: Delegate? всегда опционален: ARC сам обнулит ссылку, когда объект освободится. Поэтому протокол делегата ограничивают классами (AnyObject).

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

  • ! для данных с сервера: «обычно приходит» не гарантия.
  • try? при декодировании: ошибка теряется, и непонятно, какое поле сломалось.
  • Вложенные if let вместо одного guard let с несколькими условиями.
  • Сравнение двойного опционала с nil и удивление результату.

Что спросят на собесе

  • Что такое опционал и как он устроен внутри?
  • Разница if let и guard let, когда что использовать?
  • Когда допустим force unwrap, что такое IUO?
  • Что вернёт Int("12a") ?? -1 и какой тип у цепочки a?.b?.c?
  • Чем map отличается от flatMap у Optional?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.