Опционалы в Swift
Несмотря на некоторый опыт в мобильной разработке (в том числе с применением Swift), регулярно на почве свифтовых опционалов возникали ситуации, когда я знал что нужно делать, но не совсем внятно представлял, почему именно так. Приходилось отвлекаться и углубляться в документацию — количество «заметок на полях» пополнялось с удручающей периодичностью. В определенный момент они достигли критической массы, и я решил упорядочить их в едином исчерпывающем руководстве. Материал получился довольно объемным, поскольку предпринята попытка раскрыть тему максимально подробно. Статья будет полезна как начинающим Swift-разработчикам, так и матерым профессионалам из мира Objective-C — есть ненулевая вероятность, что и последние найдут для себя что-то новое. А если не найдут, то добавят свое новое в комментарии, и всем будет польза.
Что такое Optionals?
Optionals (опционалы) — это удобный механизм обработки ситуаций, когда значение переменной может отсутствовать. Значение будет использовано, только если оно есть.
Зачем нужны Optionals, когда есть проверка на nil?
Во-первых, проверка на равенство/неравенство nil
применима только к nullable-типам и не применима к примитивным типам, структурам и перечислениям. Для обозначения отсутсвия значения у переменной примитивного типа приходится вводить спецзначения, такие как NSNotFound.
NSNotFound не только нужно рассматривать как спецзначение, но и следить, чтобы оно не входило в множество допустимых значений переменной. Ситуация усложняется еще и тем, что NSNotFound считается равным NSIntegerMax, т.е. может иметь разные значения для разных (32-bit/64-bit) платформ. Это значит, что NSNotFound нельзя напрямую записывать в файлы и архивы или использовать в Distributed Objects.
Соответственно, пользователь этой переменной должен учитывать, что спецзначения возможны. В Swift даже примитивный тип можно использовать в опциональном стиле, т.е явным образом указывать на то, что значения может не быть.
Во-вторых: явная опциональность проверяется на этапе компиляции, что снижает количество ошибок в runtime. Опциональную переменную в Swift нельзя использовать точно так же, как неопциональную (за исключением неявно извлекамых опционалов, подробности в разделе Implicit Unwrapping). Опционал нужно либо принудительно преобразовывать к обычному значению, либо использовать специальные преобразующие идиомы, такие как if let
, guard let
и ??
. Опционалы в Swift реализуют не просто проверку, но целую парадигму опционального типа в теории типов.
В-третьих, опционалы синтаксически более лаконичны, чем проверки на nil
, что особенно хорошо видно на цепочках опциональных вызовов — так называемый Optional Chaining.
Как это работает?
Опционал в Swift представляет из себя особый объект-контейнер, который может содержать в себе либо nil
, либо объект конкретного типа, который указывается при объявлении этого контейнера. Эти два состояния обозначаются терминами None и Some соответственно. Если при создании опциональной переменной присваемое значение не указывать, то nil
присваивается по умолчанию.
В документации значение по умолчанию в случае отсутсвия явного присвоения не упоминается, но сказано, что опционал represents either a wrapped value or nil, the absence of a value. Если опциональная переменная объявлена без явного присвоения (какое-либо Some не присваивалось), то логично следует, что неявно присваивается None — третьего «неинициализрованного» состояния у опционалов нет.
Опционал объявляется посредством комбинации имени типа и лексемы ?
. Таким образом, запись Int?
— это объявление контейнера, экземпляр которого может содержать внутри nil
(состояние None Int) либо значение типа Int
(состояние Some Int). Именно поэтому при преобразовании Int?
в Int
используетя термин unwrapping вместо cast, т.е. подчеркивается «контейнерная» суть опционала. Лексема nil
в Swift обозначает состояние None, которое можно присвоить любому опционалу. Это логично приводит к невозможности присвоить nil
(состояние None) переменной, которая не является опционалом.
По факту опционал представляет собой системное перечисление:
public enum Optional : ExpressibleByNilLiteral {
/// The absence of a value.
///
/// In code, the absence of a value is typically written using the `nil`
/// literal rather than the explicit `.none` enumeration case.
case none
/// The presence of a value, stored as `Wrapped`.
case some(Wrapped)
/// Creates an instance that stores the given value.
public init(_ some: Wrapped)
...
/// Creates an instance initialized with `nil`.
///
/// Do not call this initializer directly. It is used by the compiler
// when you initialize an `Optional` instance with a `nil` literal.
public init(nilLiteral: ())
...
}
Перечисление Optional
имеет два возможных состояния: .none
и some(Wrapped)
. Запись Wrapped?
обрабатывается препроцессором (Swift«s type system) и трансформируется в Optional
, т.е. следующие записи эквивалентны:
var my_variable: Int?
var my_variable: Optional
Лексема nil
по факту обозначает Optional.none
, т.е. следующие записи эквивалентны:
var my_variable: Int? = nil
var my_variable: Optional = Optional.none
var my_variable = Optional.none
Перечисление Optional
имеет два конструктора. Первый конструктор init(_ some: Wrapped)
принимает на вход значение соответсвующего типа, т.е. следующие записи эквивалентны:
var my_variable = Optional(42) // тип .some-значения Int опеределен неявно
var my_variable = Optional(42) // явное указание типа Int для наглядности
var my_variable = Int?(42) // тип Int необходимо указать явно
var my_variable: Int? = 42 // тип Int необходимо указать явно
var my_variable = Optional.some(42) // тип Int опеределен неявно
var my_variable = Optional.some(42) // явное указание типа для наглядности
Второй конструктор init(nilLiteral: ())
является реализацией протокола ExpressibleByNilLiteral
public protocol ExpressibleByNilLiteral {
/// Creates an instance initialized with `nil`.
public init(nilLiteral: ())
}
и инициализирует опциональную переменную состоянием .none
. Этот конструктор используется только компилятором. Согласно документации его нельзя вызывать напрямую, т.е следующие записи приведут к ошибкам компиляции:
var my_variable1 = Optional(nil) // ошибка компиляции
var my_variable2 = Optional.none(nil)// ошибка компиляции
Вместо них следует использовать
var my_variable: Int? = nil // или var my_variable: Int? = Optional.none
или вообще не использовать явное присвоение
var my_variable: Int?
поскольку nil
будет присвоен по умолчанию.
Перечисление Optional
также содержит свойство unsafelyUnwrapped, которое предоставляет доступ на чтение к .some
-значению опционала:
public enum Optional : ExpressibleByNilLiteral {
...
/// The wrapped value of this instance, unwrapped without checking whether
/// the instance is `nil`.
public var unsafelyUnwrapped: Wrapped { get }
}
Если опционал находится в состоянии .none
, обращение к unsafelyUnwrapped
приведет к серьезному сбою программы.
В режиме отладки debug build -Onone будет ошибка рантайма:
_fatal error: unsafelyUnwrapped of nil optional_
В релизной сборке optimized build -O будет ошибка рантайма либо неопределенное поведение. Более безопасной операцией является Force Unwrapping (или Explicit Unwrapping) — принудительное извлечение .some
-значения, обозначаемое лексемой !
. Применение Force Unwrapping к опционалу в состоянии .none
приведет к ошибке рантайма:
_fatal error: unexpectedly found nil while unwrapping an Optional value_
let my_variable1 = Int?(42) // содержит 42, тип Optional Int
let my_value1A = my_variable1! // содержит 42, тип Int
let my_value1B = my_variable1.unsafelyUnwrapped // содержит 42, тип Int
let my_variable2 = Int?.none // содержит nil, тип Optional Int
let my_value2A = my_variable2! // ошибка рантайма
// ошибка рантайма в режиме -Onone, НЕОПРЕДЕЛЕННОЕ ПОВЕДЕНИЕ в режиме -O
let my_value2B = my_variable2.unsafelyUnwrapped
Идиомы использования
Нет особого смысла использовать обычное перечисление с двумя состояниями. Вполне можно реализовать подобный механизм самостоятельно: создать enum c двумя состояниями и конструкторами для соответствующих значений, добавить какой-нибудь постфиксный оператор для Force Unwrapping (например, как это сделано здесь), добавить возможность сравнения с nil
или вообще придумать «свой» nil
и т.д. Опционалы должны быть интегрированы непосредственно в сам язык, чтобы их использование было естественным, не чужеродным. Разумеется, можно рассматривать такую интеграцию как «синтаксический сахар», однако языки высокого уровня для того и существуют, чтобы писать (и читать) код на них было легко и приятно. Использование опционалов в Swift подразумевает ряд идиом или особых языковых конструкций, которые помогают уменьшить количество ошибок и сделать код более лаконичным. К таким идиомам относятся Implicit Unwrapping, Optional Chaining, Nil-Coalescing и Optional Binding.
Implicit unwrapping
Безопасное использование Force Unwrapping подразумевает предварительную проверку на nil
, например, в условии if
:
// getOptionalResult() может вернуть nil
let my_variable: Int? = getOptionalResult() // тип Optional Int
if my_variable != nil {
// my_value получит .some-значение опционала из getOptionalResult()
let my_value = my_variable!
} else {
// ошибка рантайма
let my_value = my_variable!
}
Иногда из структуры программы очевидным образом следует, что переменная технически является опционалом, но к моменту первого использования всегда находится в состоянии .some
, т.е. не является nil
. Для использования опционала в неопциональном контексте (например, передать его в функцию с параметром неопционального типа) приходится постоянно применять Force Unwrapping с предварительной проверкой, что скучно и утомительно. В этих случаях можно применить неявно извлекаемый опционал — Implicitly Unwrapped Optional. Неявно извлекамый опционал объявляется посредством комбинации имени типа и лексемы !
:
let my_variable1: Int? = 42 // тип Optional Int
let my_variable2: Int! = 42 // тип Implicitly Unwrapped Optional Int
var my_variable3: Int! = 42 // тип Implicitly Unwrapped Optional Int
...
my_variable3 = nil // где-то непредвиденно присвоен nil
...
func sayHello(times:Int) {
for _ in 0...times {
print("Hello!")
}
}
sayHello(times: my_variable1!) // обязаны извлекать значение явно
sayHello(times: my_variable1) // ошибка компиляции
sayHello(times: my_variable2!) // можем, но не обязаны извлекать значение явно
sayHello(times: my_variable2) // неявное извлечение
sayHello(times: my_variable3) // ошибка рантайма
В вызове sayHello(times: my_variable2)
извлечение значения 42
из my_variable2
все равно осуществляется, только неявно. Использование неявно извлекаемых опционалов делает код более удобным для чтения — нет восклицательных знаков, которые отвлекают внимание (вероятно, читающего код будет беспокоить использование Force Unwrapping без предварительной проверки). На практике это скорее анти-паттерн, увеличивающий вероятность ошибки. Неявно извлекаемый опционал заставляет компилятор «закрыть глаза» на то, что опционал используется в неопциональном контексте. Ошибка, которая может быть выявлена во время компиляции (вызов sayHello(times: my_variable1)
), проявится только в рантайме (вызов sayHello(times: my_variable3)
). Явный код всегда лучше неявного. Логично предположить, что такое снижение безопасности кода требуется не только ради устранения восклицательных знаков, и это действительно так.
Неявно извлекаемые опционалы позволяют использовать self
в конструкторе для иницализации свойств и при этом:
- не нарушать правил двухэтапной инициализации (в конструкторе все свойства должны быть инициализированы до обращения к
self
) — иначе код просто не скомпилируется; - избежать излишней опциональности в свойстве, для которого она не требуется (по своему смыслу значение свойства не может отсутствовать).
Наглядный пример, где требуется использовать self
в конструкторе для иницализации свойств, приведен в документации:
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"
В этом примере экземпляры классов Country и City должны иметь ссылки друга на друга к моменту завершения инициализации. У каждой страны обязательно должна быть столица и у каждой столицы обязательно должна быть страна. Эти связи не являются опциональными — они безусловны. В процессе инициализации объекта country
необходимо инициализировать свойство capitalCity
. Для инициализации capitalCity
нужно создать экземпляр класса City. Конструктор City в качестве параметра требует соответствующий экземпляр Country, т.е. требует доступ к self
. Сложность в том, что экземпляр Country на этот момент еще не до конца инициализирован, т.е. self
использовать нельзя.
Эта задача имеет изящное решение: capitalCity
объявляется мутабельным неявно извлекаемым опционалом. Как и любой мутабельный опционал, capitalCity
по умолчанию инициализируется состоянием nil
, т. е. к моменту вызова конструктора City все свойства объекта country
уже инициализированы. Требования двухэтапной инициализации соблюдены, конструктор Country находится во второй фазе — можно передавать self
в конструктор City. capitalCity
является неявным опционалом, т.е. к нему можно обращаться в неопциональном контексте без добавления !
.
Побочным эффектом использования неявно извлекаемого опционала является «встроенный» assert
: если capitalCity
по каким-либо причинам останется в состоянии nil
, это приведет к ошибке рантайма и аварийному завершению работы программы.
Другим примером оправданного использования неявно извлекаемых опционалов могут послужить инструкции @IBOutlet
: контекст их использования подразумевает, что переменной к моменту первого обращения автоматически будет присвоено .some
-значение. Если это не так, то произойдет ошибка рантайма. Автоматическая генерация кода в Interface Builder создает свойства с @IBOutlet
именно в виде неявных опционалов. Если такое поведение неприемлемо, свойство с @IBOutlet
можно объявить в виде явного опционала и всегда обрабатывать .none
-значения явным образом. Как правило, все же лучше сразу получить «падение», чем заниматься долгой отладкой в случае случайно отвязанного @IBOutlet
-свойства.
Optional Chaining
Optional Chaining — это процесс последовательных вызовов по цепочке, где каждое из звеньев возвращает опционал. Процесс прерывается на первом опционале, находящемся в состоянии nil
— в этом случае результатом всей цепочки вызовов также будет nil
. Если все звенья цепочки находятся в состоянии .some
, то результирующим значением будет опционал с результатом последнего вызова. Для формирования звеньев цепочки используется лексема ?
, которая помещается сразу за вызовом, возвращающим опционал. Звеньями цепочки могут любые операции, которые возвращают опционал: обращение к локальной переменной (в качестве первого звена), вызовы свойств и методов, доступ по индексу.
Optional сhaining всегда работает последовательно слева направо. Каждому следующему звену передается .some
-значение предыдущего звена, при этом результирующее значение цепочки всегда является опционалом, т.е. цепочка работает по следующим правилам:
- первое звено должно быть опционалом;
- после лексемы
?
должно быть следующее звено; - если звено в состояни
.none
, то цепочка прерывает процесс вызовов и возвращаетnil
; - если звено в состояни
.some
, то цепочка отдает.some
-значение звена на вход следующему звену (если оно есть); - если результат последнего звена является опционалом, то цепочка возвращает этот опционал;
- если результат последнего звена не является опционалом, то цепочка возвращает этот результат, «завернутый» в опционал (результат вычислений присваивается
.some
-значению возвращаемого опционала).
// цепочка из трех звеньев: первое звено — опционал `country.mainSeaport?`,
country.mainSeaport?.nearestVacantPier?.capacity
// ошибка компиляции, после `?` должно быть следующее звено
let mainSeaport = country.mainSeaport?
// цепочка вернет `nil` на первом звене
country = Country(name: "Mongolia")
let capacity = country.mainSeaport?.mainPier?.capacity
// цепочка вернет опционал — ближайший незанятый пирс в Хельсинки
country = Country(name: "Finland")
let nearestVacantPier = country.mainSeaport?.nearestVacantPier
// цепочка вернет опционал — количество свободных мест, даже если capacity
// является неопциональным значением
country = Country(name: "Finland")
let capacity = country.mainSeaport?.nearestVacantPier?.capacity
Важно отличать цепочки опциональных вызовов от вложенных опционалов. Вложенный опционал образуется, когда .some
-значением одного опционала является другой опционал:
let valueA = 42
let optionalValueA = Optional(valueA)
let doubleOptionalValueA = Optional(optionalValueA)
let tripleOptionalValueA = Optional(doubleOptionalValueA)
let tripleOptionalValueB: Int??? = 42 // три `?` означают тройную вложенность
let doubleOptionalValueB = tripleOptionalValueB!
let optionalValueB = doubleOptionalValueB!
let valueB = optionalValueB!
print("\(valueA)") // 42
print("\(optionalValueA)") // Optional(42)
print("\(doubleOptionalValueA)") // Optional(Optional(42))
print("\(tripleOptionalValueA)") // Optional(Optional(Optional(42)))
print("\(tripleOptionalValueB)") // Optional(Optional(Optional(42)))
print("\(doubleOptionalValueB)") // Optional(Optional(42))
print("\(optionalValueB)") // Optional(42)
print("\(valueB)") // 42
Optional сhaining не увеличивает уровень вложенности возвращаемого опционала. Тем не менее, это не исключает ситуации, когда результирующим значением какого-либо звена является опционал с несколькими уровнями вложенности. В таких ситуациях для продолжения цепочки необходимо прописать ?
в количестве, равном количеству уровней вложенности:
let optionalAppDelegate = UIApplication.shared.delegate
let doubleOptionalWindow = UIApplication.shared.delegate?.window
let optionalFrame = UIApplication.shared.delegate?.window??.frame // два '?'
print("\(optionalAppDelegate)") // Optional( ... )
print("\(doubleOptionalWindow)") // Optional(Optional( ... ))
print("\(optionalFrame)") // Optional( ... )
Вообще говоря, необязательно, чтобы все уровни вложенности были «развернуты» с помощью лексемы ?
.Часть из них можно заменить на принудительное извлечение !
, что сократит количество «неявных» звеньев в цепочке. Отдельный вопрос, есть ли в этом смысл.
Цепочка UIApplication.shared.delegate?.window??.frame
фактически состоит из четырех звеньев: UIApplication.shared.delegate?
, .frame
и два звена, объединенных в одном вызове .window??
. Второе «двойное» звено представлено опционалом второго уровня вложенности.
Важной особенностью этого примера также является особый способ формирования двойного опционала, отличающийся от способа формирования doubleOptionalValue
в предыдущем примере. UIApplication.shared.delegate!.window
является опциональным свойством, в котором возвращается опционал. Опциональность свойства означает, что может отсутствовать само свойство, а не только .some
-значение у опционала, возвращаемого из свойства. Опциональное свойство, также как и все прочие свойства, может возвращать любой тип, не только опциональный. Опциональность такого рода формируется в @objc-протоколах с помощью модификатора optional
:
public protocol UIApplicationDelegate : NSObjectProtocol {
...
@available(iOS 5.0, * )
optional public var window: UIWindow? { get set } // модификатор optional
...
}
В протоколах с опциональными свойствами и методами (иначе, опциональными требованиями) модификатор @objc
указывается для каждого опционального требования и для самого протокола. На протокол UIApplicationDelegate из примера выше это требование не распространяется, т.к. он транслируется в Swift из системной библиотеки на Objective-C. Вызов нереализованного опционального требования у объекта, принимающего такой протокол, возвращает опционал соответствующего типа в состоянии .none
. Вызов реализованного опционального требования возвращает опционал соответсвующего типа в состоянии .some
. Таким образом, опциональные свойства и методы, в отличие от optional сhaining, увеличивают уровень вложенности возвращаемого опционала. Опциональный метод, как и свойство, «заворачивается» в опционал полностью — в .some
-значение помещается метод целиком, а не только возращаемое значение:
@objc
public protocol myOptionalProtocol {
@objc
optional var my_variable: Int { get }
@objc
optional var my_optionalVariableA: Int? { get } // ошибка компиляции:
// модификатор @objc не применим к опционалу типа Int?, т.к. Int
// не является классом
@objc
optional var my_optionalVariableB: UIView? { get }
@objc
optional func my_func() -> Int
@objc
optional func my_optionalResultfuncA() -> Int? // ошибка компиляции:
// модификатор @objc не применим к опционалу типа Int?, т.к. Int
// не является классом
@objc
optional func my_optionalResultfuncB() -> UIView?
@objc
optional init(value: Int) // ошибка компиляции:
// модификатор optional не применим инициализаторам
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, myOptionalProtocol {
var window: UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let protocolAdoption = self as myOptionalProtocol
// Optional
print("\(type(of: protocolAdoption.my_variable))")
// Optional>
print("\(type(of: protocolAdoption.my_optionalVariableB))")
// Optional<() -> Int>
print("\(type(of: protocolAdoption.my_func))")
// Optional
print("\(type(of: protocolAdoption.my_func?()))")
// Optional<() -> Optional>
print("\(type(of: protocolAdoption.my_optionalResultfuncB))")
// Optional
print("\(type(of: protocolAdoption.my_optionalResultfuncB?()))")
return true
}
}
Для опциональных @objc
-протоколов имеется ряд ограничений, в виду того, что они были введены в Swift специально для взаимодействия с кодом на Objective-C:
- могут быть реализованы только в классах, унаследованных от классов Objective-C, либо других классов с аттрибутом
@objc
(т.е. не могут быть реализованы в структурах и перечислениях); - модификатор
optional
неприменим к требованиям-конструкторамinit
; - cвойства и методы с аттрибутом
@objc
имеют ограничение на тип возвращаемого опционала — допускаются только классы.
Попытка применить Force Unwrapping на нереализованном свойстве или методе приведет к точно такой же ошибке рантайма, как и применение Force Unwrapping на любом другом опционале в состоянии .none
.
Nil-Coalescing
Оператор Nil-Coalescing возвращает .some
-значение опционала, если опционал в состоянии .some
, и значение по умолчанию, если опционал в состоянии .none
. Обычно Nil-Coalescing более лаконичен, чем условие if else
, и легче воспринимается, чем тернарный условный оператор ?
:
let optionalText: String? = tryExtractText()
// многословно
let textA: String
if optionalText != nil {
textA = optionalText!
} else {
textA = "Extraction Error!"
}
// в одну строку, но заставляет думать
let textB = (optionalText != nil) ? optionalText! : "Extraction Error!"
// кратко и легко воспринимать
let textC = optionalText ?? "Extraction Error!"
Тип значения по умолчания справа должен соответствовать типу .some
-значения опционала слева. Значение по умолчанию тоже может быть опционалом:
let optionalText: String?? = tryExtractOptionalText()
let a = optionalText ?? Optional("Extraction Error!")
Возможность использовать выражения в качестве правого операнда позволяет создавать цепочки из умолчаний:
let wayA: Int? = doSomething()
let wayB: Int? = doNothing()
let defaultWay: Int = ignoreEverything()
let whatDo = wayA ?? wayB ?? defaultWay
Optional Binding и приведение типов
Optional Binding позволяет проверить, содержит ли опционал .some
-значение, и если содержит, извлечь его и предоставить к нему доступ через с помощью локальной переменной (обычно константной). Optional Binding работает в контексте конструкций if
, while
и guard
.
В официальной документации детали реализации Optional Binding не описаны, но можно построить модель, хорошо описывающую поведение этого механизма.
В Swift каждый метод или функция без явно заданного return
неявно возвращает пустой кортеж ()
. Оператор присваивания =
является исключением и не возвращает значение, тем самым позволяя избежать случайного присвоения вместо сравнения ==
.
Допустим, что оператор присваивания также обычно возвращает пустой кортеж, но если правый операнд является опционалом в состоянии nil
, то оператор присваивания вернет nil
. Тогда эту особенность можно будет использовать в условии if
, так как пустой кортеж расценивается как true, а nil
расценивается как false:
var my_optionalVariable: Int? = 42
// условие истинно, my_variable "привязана" к .some-значению my_optionalVariable
if let my_variable = my_optionalVariable {
print("\(my_variable)") // 42
}
my_optionalVariable = nil
// условие ложно, my_variable не создана
if let my_variable = my_optionalVariable {
print("\(my_variable)")
} else {
print("Optional variable is nil!") // Optional variable is nil!
}
Переменную, сформированную в контексте ветки true, можно будет автоматически объявить неявно извлекаемым опционалом или даже обычной переменной. Не-nil
как результат операции присвоения будет являться следствием .some
-состояния правого операнда. В ветке true .some
-значение всегда будет успешно извлечено и «привязано» к новой переменной (поэтому механизм в целом и называется Optional Binding).
Область видимости извлеченной переменной в условии if
ограничивается веткой true, что логично, поскольку в ветке false такая переменная не может быть извлечена. Тем не менее, существуют ситуации, когда нужно расширить область видимости извлеченного .some
-значения, а в ветке false (опционал в состоянии .none
) завершить работу функции. В таких ситуациях удобно воспользоваться условием guard
:
let my_optionalVariable: Int? = extractOptionalValue()
// чересчур многословно
let my_variableA: Int
if let value = my_optionalVariable {
my_variableA = value
} else {
return
}
print(my_variableA + 1)
// лаконично
guard let my_variableB = my_optionalVariable else {
return
}
print(my_variableB + 1)
В Swift гарантированное компилятором приведение типов (например, повышающее приведение или указание типа литерала) выполняется с помощью оператора as
. В случаях, когда компилятор не может гарантировать успешное приведение типа (например, понижающее приведение), используется либо оператор принудительного приведения as!
, либо оператор опционального приведения as?
. Принудительное приведение работает в стиле Force Unwrapping, т.е. в случае невозможности выполнить приведение приведет к ошибке рантайма, в то время как опциональное приведение в этом случае вернет nil
:
class Shape {}
class Circle: Shape {}
class Triangle: Shape {}
let circle = Circle()
let circleShape: Shape = Circle()
let triangleShape: Shape = Triangle()
circle as Shape // гарантированное приведение
42 as Float // гарантированное приведение
circleShape as Circle // ошибка компиляции
circleShape as! Circle // circle
triangleShape as! Circle // ошибка рантайма
circleShape as? Circle // Optional
triangleShape as? Circle // nil
Таким образом, оператор опционального приведения as?
порождает опционал, который часто используется в связке с Optional Binding:
class Shape {}
class Circle: Shape {}
class Triangle: Shape {}
let circleShape: Shape = Circle()
let triangleShape: Shape = Triangle()
// условие истинно, успешное приведение
if let circle = circleShape as? Circle {
print("Cast success: \(type(of: circle))") // Cast success: (Circle #1)
} else {
print("Cast failure")
}
// условие ложно, приведение не удалось
if let circle = triangleShape as? Circle {
print("Cast success: \(type(of: circle))")
} else {
print("Cast failure") // Cast failure
}
map и flatMap
Методы map
и flatMap
условно можно отнести к идиомам Swift, потому что они определены в системном перечислении Optional:
public enum Optional : ExpressibleByNilLiteral {
...
/// Evaluates the given closure when this `Optional` instance is not `nil`,
/// passing the unwrapped value as a parameter.
///
/// Use the `map` method with a closure that returns a nonoptional value.
public func map(_ transform: (Wrapped) throws -> U) rethrows -> U?
/// Evaluates the given closure when this `Optional` instance is not `nil`,
/// passing the unwrapped value as a parameter.
///
/// Use the `flatMap` method with a closure that returns an optional value.
public func flatMap(_ transform: (Wrapped) throws -> U?) rethrows -> U?
...
}
Данные методы позволяют осуществлять проверку опционала на наличие .some
-значения и обрабатывать это значение в замыкании, полученном в виде параметра. Оба метода возвращают nil
, если исходный опционал nil
. Разница между map
и flatmap
заключается в возможностях замыкания-параметра: для flatMap
замыкание может дополнительно возвращать nil
(опционал), а для map
замыкание всегда возвращает обычное значение:
let my_variable: Int? = 4
let my_squareVariable = my_variable.map { v in
return v * v
}
print("\(my_squareVariable)") // Optional(16)
let my_reciprocalVariable: Double? = my_variable.flatMap { v in
if v == 0 {
return nil
}
return 1.0 / Double(v)
}
print("\(my_reciprocalVariable)") // Optional(0.25)
Выражения с map
и flatmap
обычно более лаконичны, чем конструкции с предварительной проверкой в if
или guard
, и воспринимаются легче, чем конструкции с тернарным условным оператором:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd MMM yyyy"
let date: Date? = extractOptionalDate()
// многословно
let dateStringA: String
if date != nil {
dateStringA = dateFormatter.string(from: date!)
} else {
dateStringA = "Unknown date"
}
// в одну строку, но сложно воспринимать
let dateStringB =
(date == nil ? nil : dateFormatter.string(from: date!)) ?? "Unknown date"
// лаконично, с непривычки сложно воспринимать (если map используется редко)
let dateStringC = date.map(dateFormatter.string) ?? "Unknown date"
Уместность той или иной идиомы во многом зависит от договоренностей по стилю кодирования, принятых на проекте. Вполне возможно, что явные проверка опционала и работа с извлеченным .some
-значением будут выглядеть естественнее, чем применение map
или flatmap
:
// отдельно Optional Binding и явный вызов метода у извлеченного объекта
func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let cell = sender as? UITableViewCell,
let indexPath = tableView.indexPathForCell(cell) {
let item = items[indexPath.row]
}
}
// В одном вызове 3 этапа:
// 1) опционал как результат приведения типов;
// 2) передача неявного замыкания в метод flatMap полученного опционала;
// 3) Optional Binding к результату flatMap.
func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let indexPath =
(sender as? UITableViewCell).flatMap(tableView.indexPathForCell) {
let item = items[indexPath.row]
}
}
Опционалы и обработка исключений
Swift поддерживает несколько способов обработки исключений, и один из них — это преобразование исключения в nil
. Такое преобразование можно осуществить автоматически с помощью оператора try?
(примеры из документации):
func someThrowingFunction() throws -> Int {
// ...
}
// многословно
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
// лаконично
let x = try? someThrowingFunction()
Обе переменные x
и y
являются опциональными, независимо от того, какой тип возвращает someThrowingFunction()
. Таким образом, семантика поведения оператора try?
такая же, как и у оператора as?
. Логично также наличие оператора try!
, который позволяет проигнорировать возможность выброса исключения из функции. Если исключение все же будет выброшено, то произойдет ошибка рантайма:
// ошибка рантайма, если loadImage будет выброшено исключение
// photo не является опционалом (в отличие от x и y)
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
Опционалы и Objective-C
В Objective-C нет понятия опциональности. Лексема nil
в Objective-C обозначает нулевой указатель, т.е. обращение к любой переменной ссылочного типа потенциально может вернуть nil
. В Swift nil
обозначает опционал в состоянии .none
, неопциональная переменная не может быть nil
, поэтому до Xcode 6.3 любой указатель из Objective-C транслировался в Swift как неявно извлекаемый опционал. В Xcode 6.3 в Objective-C для совместимости с семантикой опционалов были введены так называемые nullability annotations:
@interface myObject : NSObject
@property (copy, readonly) NSArray * _Nullable myValuesA;
@property (copy, readonly) NSString * _Nonnull myStringA;
@property (copy, readonly, nullable) NSArray * myValuesB;
@property (copy, readonly, nonnull) NSString * myStringB;
@end
К ним относятся nullable
(или _Nullable
), nonnull
(или _Nonnull
), а также null_unspecified
и null_resettable
. Nullability-aннотациями могут быть обозначены ссылочные типы в свойствах, а также в параметрах и результатах функций. Помимо отдельных аннотаций можно использовать специальные макросы NS_ASSUME_NONNULL_BEGIN
и NS_ASSUME_NONNULL_END
для пометки участков кода целиком. Аннотации не являются полноценными модификаторами указателей или аттрибутами свойств, поскольку не влияют на компиляцию кода на Objective-C (если не считать предупреждений компиляции, например, при попытке присвоить nil
свойству с аннотацией nonnull).
Аннотация null_resettable
подразумевает, что сеттер свойства может принимать nil
, но при этом геттер свойства вместо nil
возвращает некоторое значение по умолчанию.
Трансляция из Objective-C в Swift осуществлятся по следующим правилам:
- значения из области между
NS_ASSUME_NONNULL_BEGIN
иNS_ASSUME_NONNULL_END
импортируются в виде неопциональных (обычных) значений; - значения с аннотациями
nonnull
или_Nonnull
импортируются в виде неопциональных (обычных) значений; - значения с аннотациями
nullable
или_Nullable
импортируются в виде опционалов; - значения с аннотацией
null_resettable
импортируются в виде неявно извлекаемых опционалов; - значения без nullability-aннотаций или аннотацией
null_unspecified
(аннотация по умолчанию) импортируются в виде неявно извлекаемых опционалов.
Правила передачи опционалов из Swift в Objective-C несколько проще:
- если опционал в состоянии
.none
, то возвращается экземпляр NSNull; - если опционал в состоянии
.some
, то возвращается указатель на.some
-значение.
Резюме, синтаксис
Лексема !
может быть использована в четырех контекстах, связанных с опциональностью:
- для принудительного извлечения значения из опционала;
- для объявления неявного опционала;
- для принудительной конвертации типов в операторе
as!
; - для принудительного подавления исключения в операторе
try!
.
Унарный оператор логического отрицания !
не считается, поскольку относится к другому контексту.
Лексема ?
может быть использована в четырех контекстах, связанных с опциональностью:
- для объявления явного опционала;
- для использования опционала в optional chaining;
- для опциональной конвертации типов в операторе
as?
; - для преобразования исключения в
nil
в оператореtry?
.
Тернарный условный оператор ?
не считается, поскольку относится к другому контексту.
Лексема ??
может быть использована в двух контекстах: