Особенности Swift, Часть 1: Опционалы

Особенности Swift, Часть 1: Опционалы

Особенности Swift, Часть 1: Опционалы

Особенности Swift - серия статей о различных аспектах Swift. Я выбрал опционалы темой первой главы, поскольку, как вы заметили, они являются одним из первых особенностей Swift. Идея опционалов проста: чтобы избежать ошибок и точнее проектировать блоки кода, Swift различает опциональные и не опциональные значения.

Что такое Опционалы?

В Objective-C, нет явного понятия опциональных значений. Чтобы показать, где это может создавать проблемы, давайте взглянем на метод, который возвращает индекс элемента item в списке дел:

  1. - (NSInteger)indexOfItem:(Item *item) {
  2. // ...
  3. }

Здесь, item неявно является опционалом: мы можем либо передать экземпляр класса Item, либо nil. С другой стороны, возвращаемое значение неявно является не опциональным: поскольку NSInteger не является типом объекта, нет никакого способа не возвращать значение.

Для параметра item, имело бы смысл требовать значение не nil. Но это невозможно в Objective-C. Для возвращаемого значения, хотелось бы иметь возможность вернуть nil, если элемента item нет в списке. Но это невозможно в Objective-C. Мы можем только задокументировать что произойдет если передать nil в качестве item (ошибка или нет?), и какое значение вернется, в случае, если значение отсутствует в списке (NSNotFound который объявлен как NSIntegerMax? Или NSIntegerMin? Или -1?).

Разве не было бы здорово, если бы мы должны были явно указывать какие значения будут опциональными. Так именно и происходит в Swift. Тип Т не опциональный. Чтобы разрешить отсутствие значения, мы используем опциональный тип T?. Мы можем объявить метод следующим образом:

  1. func indexOfItem(item: Item) -> Int? {
  2.  // ...
  3. }

Здесь, объявление метода нуждается в том, чтобы мы ему передали item. Попытка передать nil, приведет к ошибке компиляции. Кроме того, метод явно показывает, что возвращаемое значение опциональное, так что нам не нужно объявлять специальный Int, чтобы показать отсутствие значения.

Переменная v типа T? имеет несколько отличий от переменной не опционального типа T:

  1. Она может содержать не только значения типа T, но и пустое значение nil
  2. Она инициализируется с nil.
  3. Чтобы получить доступ к значению типа T, которое может храниться в v, мы сперва должны извлечь значение.

Извлечение опционалов

Скажем, мы хотим написать метод, который вернет индекс элемента в списке, который начинается с единицы (давайте назовем его натуральный индекс, поскольку это более естественно начинать отсчет с 1) В Objective-C, реализация этого метода может выглядеть так:

  1. - (NSInteger)naturalIndexOfItem:(Item *)item {
  2.  NSInteger indexOrNotFound = [self indexOfItem:item];
  3.  if (indexOrNotFound != NSNotFound) {
  4.   return indexOrNotFound + 1;
  5.  } else {
  6.   return NSNotFound;
  7.  }
  8. }

Если мы напишем соответствующий код на Swift, в качестве первой попытки это может выглядеть так:

  1. func naturalIndexOfItem(item: Item) -> Int? {
  2.  let indexOrNotFound = indexOfItem(item)
  3.  if indexOrNotFound {
  4.   let index = indexOrNotFound!
  5.   return index + 1
  6.  } else {
  7.   return nil
  8.  }
  9. }

Мы можем использовать опциональные переменные в качестве логического выражения непосредственно внутри оператора if, для того чтобы выявить присутствует ли значение. Восклицательный знак (!) после indexNotFound извлекает опциональное значение и приводит к не опциональному. Так, index имеет тип Int, и компилятор не будет ругаться на операцию сложения.

Если бы indexOrNotFound был nil, тогда бы извлечение потерпело неудачу с выводом runtime ошибки. По этой причине, выражение indexNotFound! называют выражение принудительного извлечения.

Привязка опционалов

Конечно же, первая попытка это не лучшее решение: код теперь длиннее чем в Objective-C. Мы можем сделать намного лучше. Во-первых, поскольку нам часто нужно извлечь значение и потом использовать его, Swift предусматривает нечто называемое привязка опционалов:

  1. func naturalIndexOfItem(item: Item) -> Int? {
  2.  let indexOrNotFound = indexOfItem(item)
  3.  if let index = indexOrNotFound {
  4.   return index + 1
  5.  } else {
  6.   return nil
  7.  }
  8. }

Если мы используем let или var внутри условия оператора if, то константа или переменная устанавливаются на извлеченное значение, так что нам больше не нужно извлекать ее. Теперь мы можем сократить indexOrNotFound:

  1. func naturalIndexOfItem(item: Item) -> Int? {
  2.  if let index = indexOfItem(item) {
  3.   return index + 1
  4.  } else {
  5.   return nil
  6.  }
  7. }

Вместо прибавления 1, мы можем вызвать successor() для index:

  1. func naturalIndexOfItem(item: Item) -> Int? {
  2.  if let index = indexOfItem(item) {
  3.   return index.successor()
  4.  } else {
  5.   return nil
  6.  }
  7. }

Но даже этот способ занимает много кода в Swift. Переходим к сцеплению опционалов.

Сцепление опционалов

Суть описанного выше метода - вызов функции successor для indexOfItem(item). В остальном, это просто обработка опционала. Тем не менее, мы не можем писать:

  1. func naturalIndexOfItem(item: Item) -> Int? {
  2.  return indexOfItem(item).successor()
  3. }

Это приведет к ошибке компиляции, поскольку метод successor не был объявлен для опционального типа возвращаемого indexOfltem(item). И он не будет компилироваться, так как мы проигнорировали наличие опционала. Как насчет извлечения с помощью выражения принудительного извлечения?

  1. func naturalIndexOfItem(item: Item) -> Int? {
  2.  return indexOfItem(item)!.successor()
  3. }

Теперь, как только элемента item не окажется в списке, мы получим runtime error. Это заработет только за счет сцепления опционалов, которое пишется с помощью ?:

  1. func naturalIndexOfItem(item: Item) -> Int? {
  2.  return indexOfItem(item)?.successor()
  3. }

Если объект а опционального типа, то a?.b вернет nil, если a будет nil, в противном случае он вернет b. В Objective-C у нас есть неявное сцепление для объектов: если у нас есть переменная item, которая хранит некий элемент или nil, которая может принадлежать списку или нет, и список может иметь значение или нет, то мы получим название списка или nil с помощью вызова:

NSString *listName = item.list.name;

На Swift, для такого же случая мы должны писать:

var listName = item?.list?.name

Разве это не логичнее? Да, конечно. Мы должны четко указать, как мы обрабатываем опционалы. Более того, мы можем отфильтровать, какие части действительно опциональные, а какие нет. Мы можем избавиться от первого вопросительного знака, сделав item не опциональной переменной. Мы можем избавиться от второго вопросительного знака, сделав свойство list не опциональным. Наконец, мы можем сделать переменную listName типа String вместо String? , сделав свойство name не опциональным.

В итоге:

В Objective-C, мы не могли делать объекты не опциональными, и мы не могли делать другие типы опциональными. В Swift, мы можем.

Неявно извлеченные опционалы

Существует второй тип опционалов в Swift: неявно извлеченный опционал. Он необходим по разным причинам, одно из них это лучшая совместимость с библиотеками Objective-C. Ярким примером является метод +currentDevice у UIDevice. В Objective-C, это выглядит так:

+ (UIDevice *)currentDevice;

Как мы можем сигнатуру такого метода отправить в Swift? Сначала попробуем:

class func currentDevice() -> UIDevice

Это не будет работать. Пока мы не будем действительно уверены, что currentDevice всегда будет возвращать значение, но это верно не для всех методов и свойств в Objective-C. Например, и метод anyObject у NSSet и свойство superview у UTView могут быть nil.

Мы можем использовать опционал:

class func currentDevice() -> UIDevice?

Это работало бы прекрасно. Недостаток в том, что наш код будет загроможден большим количеством извлечений, сцеплением опционалов и так далее, хотя большинство методов UIKit возвращают не nil объекты. Представьте, что мы хотим иметь название текущего устройства как NSString. Даже несмотря на то, что мы знаем что устройство и его название не nil, нам пришлось бы писать:

var name = UIDevice.currentDevice()!.name!

Не красиво. Мы хотели бы иметь нечто, что могло бы быть nil, но нам не пришлось его извлекать. Это как раз неявно извлеченный опционал, который обозначается восклицательным знаком:

class func currentDevice() -> UIDevice!

Переменная v типа T! имеет одно отличие от переменой опционального типа T?: она будет неявно извлечена к типу T. Благодаря этому, нам не нужно ничего здесь извлекать:

var name = UIDevice.currentDevice().name

Для переменной name теперь выведен тип NSString!, но это не проблема, так как она автоматически извлечется, когда к ней будет осуществлен доступ.

Поэтому, когда мы имеем метод или свойство Objective-C, которое, как нам известно, не возвращает nil, мы можем использовать их точно так же как не опционал. Если мы знаем, что значение может быть nil, мы можем использовать привязку и сцепление опционалов, чтобы избежать runtime ошибок.

Тем не менее, неявно извлеченные опционалы не были добавлены только для поддержки Objective-C. В Swift они помогают нам с проблемами, когда объект нуждается в ссылке на другой объект, не доступный во время инициализации. Самый простой пример, который мог бы прийти мне в голову - рекурсивное замыкание. Скажем мы хотели бы объявить факториал, и напечатать factorial(12). Сначала попробуем:

  1. var factorial: Int -> Int
  2. factorial = { x in (x == 0) ? 1 : x * factorial(x — 1) }
  3. println(factorial(12))

Компилятор справедливо жалуется на то, что мы не можем использовать факториал внутри замыкания, потому что на тот момент, переменная не инициализирована. Мы можем избежать этого, сделав factorial опциональным:

  1. var factorial: (Int -> Int)?
  2. factorial = { x in (x == 0) ? 1 : x * factorial!(x — 1) }
  3. println(factorial!(12))

Теперь наш код переполнен извлечениями, хотя очевидно, что factorial объявляется во время обращения к нему. Неявно извлеченный опционал - это лучшее решение:

  1. var factorial: (Int -> Int)!
  2. factorial = { x in (x == 0) ? 1 : x * factorial(x — 1) }
  3. println(factorial(12))

В итоге

В Swift у нас есть не опционалы, опционалы и неявно извлеченные опционалы. Это сильно отличается от Objective-C, так что, некоторые вещи могут вызвать затруднения.

Глюки

Самый большой глюк с которым мы можем столкнуться - это то, что опциональное значение вычисляется как true если значение установлено. На первый взгляд, мы можем ожидать от этого куска кода распечатки “Not authenticated”:

  1. var authenticated: Bool? = false
  2. if authenticated {
  3.  println("Authenticated")
  4. } else {
  5.  println("Not authenticated")
  6. }

Однако, он напечатает “Authenticated”, так как значение authenticated будет true, поскольку оно не nil, независимо от извлеченного значения. Чтобы обработать все три случая, нам нужно извлечь authenticated:

  1. var authenticated: Bool? = false
  2. if let authenticated = authenticated {
  3.  if authenticated {
  4.   println("Authenticated")
  5.  } else {
  6.   println("Not authenticated")
  7.  }
  8. } else {
  9.  println("Unknown")
  10. }

или

  1. var authenticated: Bool? = false
  2. if authenticated && authenticated! {
  3.  println("Authenticated")
  4. } else if authenticated {
  5.  println("Not authenticated")
  6. } else {
  7.  println("Unknown")
  8. }

Прежде чем начать жаловаться на это, мы должны понимать, что соответствующий код Objective-C с использованием NSNumber *authenticated будет выглядеть хуже.

Другой глюк в том, что вопросительный и восклицательный знаки для типа T совершенно отличаются от их аналогов использующихся для выражения v:

  • T? - это опциональный тип
  • T! - это неявно извлеченный опциональный тип
  • v? - используются для сцепления опционалов
  • v! - используется для извлечения

Вещи, которые могут запутать, когда начинаете работать со Swift:

  • Если у нас есть не опциональный тип T, мы можем использовать ? и ! для него. Если у нас есть выражение v не опционального типа T, то мы не можем использовать ? и ! для него, так как оба и сцепление и извлечение не имеет смысла для не опциональных значений.
  • Мы можем использовать ! на обоих выражениях типа Т! и Т?
  • Мы можем использовать ? на обоих выражениях типа T? и Т!

Последняя небольшая путаница это поведение ? в выражении v, когда мы используем его без вызова чего-либо для него. Тогда он просто возвращает v как опционал. Так что, если v либо типа Int? либо типа Int!, и мы объявляем

  1. var v2 = v?
  2. var v3 = v???????????

, оба v2 и v3 будут типа Int?

Рекомендации к использованию опционалов

На данный момент, Swift появился на свет всего пару месяцев назад. Так что воспринимайте следующие рекомендации с определенной долей скептицизма.

Во-первых, не используйте оператор сцепления опционалов ? сам по себе, а только с последующей точкой. Его имя уже указывает на его основное применение: цепочку опционалов.

Во-вторых, пока это технически возможно, не используйте опционалы для опционалов. Да, вы можете объявить нечто вроде: var v: Int?!!. Но не делаете этого. Пожалуйста, не нужно.

Наконец, используйте опционалы только там, где они вам нужны, и для особых случаев, которые на самом деле имеют смысл. Например, когда вы проектируете список дел, у которого есть имя и элементы, свойство items всегда может быть не опциональным массивом. Свойство name может быть не опциональный String. Делайте name опциональным значением, только тогда, когда действительно нужно рассмотреть удобный стандартный случай пустого String, и случай, когда name не установлено.

В итоге

Опционалы в Swift и неявно извлеченные опционалы прекрасны. Но не опционалы - предпочтительнее для выбора.

В следующей статье этих серий, мы рассмотрим кортежи в Swift.