Pull to refresh

Опционалы в Swift

Reading time22 min
Views87K

Несмотря на некоторый опыт в мобильной разработке (в том числе с применением 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<Wrapped> : 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<Wrapped>, т.е. следующие записи эквивалентны:


var my_variable: Int?

var my_variable: Optional<Int>

Лексема nil по факту обозначает Optional.none, т.е. следующие записи эквивалентны:


var my_variable: Int? = nil

var my_variable: Optional<Int> = Optional.none

var my_variable = Optional<Int>.none

Перечисление Optional имеет два конструктора. Первый конструктор init(_ some: Wrapped) принимает на вход значение соответсвующего типа, т.е. следующие записи эквивалентны:


var my_variable = Optional(42) // тип .some-значения Int опеределен неявно

var my_variable = Optional<Int>(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<Int>.some(42) // явное указание типа для наглядности

Второй конструктор init(nilLiteral: ()) является реализацией протокола ExpressibleByNilLiteral


public protocol ExpressibleByNilLiteral {

    /// Creates an instance initialized with `nil`.
    public init(nilLiteral: ())
}

и инициализирует опциональную переменную состоянием .none. Этот конструктор используется компилятором. Согласно документации его не рекомендуется вызывать напрямую


var test = Optional<Int>(nilLiteral: ()) // не рекомендуется

что логично, поскольку преобразование пустого кортежа Void () в nil несколько неочевидно.


Вместо этого конструктора следует использовать


var my_variable: Int? = nil // или var my_variable: Int? = Optional.none

или вообще не использовать явное присвоение


var my_variable: Int?

поскольку nil будет присвоен по умолчанию.


Перечисление Optional<Wrapped> также содержит свойство unsafelyUnwrapped, которое предоставляет доступ на чтение к .some-значению опционала:


public enum Optional<Wrapped> : 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<Int>
        print("\(type(of: protocolAdoption.my_variable))")

        // Optional<Optional<UIView>>
        print("\(type(of: protocolAdoption.my_optionalVariableB))")

        // Optional<() -> Int>
        print("\(type(of: protocolAdoption.my_func))")

        // Optional<Int>
        print("\(type(of: protocolAdoption.my_func?()))")

        // Optional<() -> Optional<UIView>>
        print("\(type(of: protocolAdoption.my_optionalResultfuncB))")

        // Optional<UIView>
        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<Circle>
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<Wrapped> : 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<U>(_ 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<U>(_ 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?.

Примечание

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


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


  • в Optional сhaining для обращения к опционалу второго уровня вложенности;
  • в качестве оператора Nil-Coalescing.

Заключение


Нулевой указатель — это ошибка на миллиард долларов. Вызывающая сторона все равно должна учитывать контекст и проверять результат на равенство специфичной константе, означающее отсутствие данных. Тот факт, что константа null всего одна, принципиально ситуацию не меняет и лишь добавляет неожиданностей при приведении типов.


Факт отсутствия данных должен обрабатываться отдельной сущностью, внешней по отношению к самим данным. В С++ или Java в область допустимых значений указателя включено специальный "адрес", обозначающий отсутствие адресата. "Правильный" указатель не может существовать без адресата, следовательно, не может "осознать" отсутствие адресата. Даже человеку, т.е. довольно сложной системе, приходится довольстоваться аксиомой Cogito, ergo sum (лат. — "Мыслю, следовательно существую"). У человека нет достоверных признаков собственного бытия или небытия, но у внешних по отношению к человеку сущностей эти критерии есть. В Swift такой внешней сущностью является опционал.


Дополнительные материалы



UPD: (by Alexander Zimin) Конструктор init(nilLiteral: ()) напрямую вызвать на самом деле можно:


var test = Optional<Int>(nilLiteral: ())

Тем не менее, в документации от Apple не рекомендуется это делать.

Tags:
Hubs:
+8
Comments4

Articles

Change theme settings