Опционал это способ сказать «значения может не быть» так, чтобы компилятор не дал об этом забыть. Большая часть крэшей в 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?