Опционалы в Swift

habr.png

Несмотря на некоторый опыт в мобильной разработке (в том числе с применением 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?.


Примечание

Тернарный условный оператор ? не считается, поскольку относится к другому контексту.


Лексема ?? может быть использована в двух контекстах:


© Habrahabr.ru