obo.dev

Опционалы (Optional). Пару слов про опциональные типы данных

05 Dec 2022

Опциональный тип данных (Optional data type)

Переменная (константа) - это указатель на область памяти, в которой хранятся некоторые данные с каким-то определенным типом.

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

Компилятор читает и выполняет код сверху вниз. Перед тем, как обратиться к переменной (константе) в какой-то части кода, её сначала необходимо инициализировать, присвоив какое-то значение.

В языке Swift мы можем объявить переменную (константу) без присвоения какого-либо значения - без инициализации. Но если попытаться обратиться к такой переменной (константе) без инициализации её с каким-то значением, то компилятор выведет ошибку.

var a: Int
print(a) // произойдет ошибка компиляции

Произойдет ошибка компиляции, так как переменная а до её вызова не была инициализирована.

Следующий код не приведет к ошибке, так как переменная а была инициализирована со значением 5.

var a: Int = 5
print(a) 

// Output
// 5

В отличие от некоторых других языков программирования, в языке Swift переменные (константы) не инициализируются со значением по умолчанию.

Часто появляются ситуации, в которых значение у переменной (константы) может отсутствовать. И так как, для инициализации переменной (константы) необходимо присвоить ей значение, то для обозначения отсутствующего значения применяется специальное значение nil в качестве значения переменной (константы).

Значение “nil” обозначает именно отсутствие значения, а не ноль, как отсутствие значения для числовых типов данных, либо пустая строка, для строковых типов данных.

Опциональный тип данных - это такой вид данных, когда значение данных может существовать, а может и не существовать.

Опционал (другими словами, опциональный тип данных) предоставляет собой определение переменной, реализующее следующие две возможности: переменная-опционал может содержать значение некоего определенного типа данных, называемого в дальнейшем базовым, либо не содержать ничего вообще. Для того чтобы показать отсутствие значение у переменной-опционала, нужно присвоить ей особое значение “nil”.

Опциональные типы представляют объекты, которые могут иметь, а могут и не иметь значение. Значение nil может применяться только к объектам опциональных типов.

Объекту опционального типа можно присвоить какое-то значение или значение “nil”. При этом тип данных у этого объекта будет опциональным, а значение будет либо “nil”, либо “Optional(value)”, где “value” - это и есть значение, помещённое в обёртку опционала.

Объявление объектов с опциональным типом

Объявление переменных или констант такое же, как и при объявлении переменных (констант) с другими типами данных.

let constant-name: dataType = expression

var variable-name: dataType = expression

Опциональный тип данных записывается так:

  • Optional<dataType> - полная форма записи;
  • dataType? - сокращённая форма записи.

Здесь “dataType” - это тип данных, помещённых в обёртку опционала. Без указания типа данных нельзя записывать опциональный тип данных.

Опционал объявляется посредством комбинации имени типа и лексемы “?”. Таким образом, запись “Int?” — это объявление контейнера, экземпляр которого может содержать внутри “nil” (состояние “None Int”) либо значение типа “Int” (состояние “Some Int”). Именно поэтому при преобразовании “Int?” в “Int” используется термин unwrapping вместо cast, т.е. подчеркивается “контейнерная” суть опционала. Лексема “nil” в Swift обозначает состояние “None”, которое можно присвоить любому опционалу. Это логично приводит к невозможности присвоить “nil” (состояние “None”) переменной, которая не является опционалом.

Пример:

var title: Optional<String> = "Some title" // полная форма записи
var subTitle: String? = nil // сокращенная форма записи
var text: String? // Объявление переменной, с последующей инициализацией
text = "Some text" // Инициализация переменной

print(title) 

// Output
// Optional("Some title")

print(subTitle)

// Output
// nil

print(text) 

// Output
// Optional("Some text")

Некоторые замечания

Если присвоить значение без указания опционального типа данных, то тип данных у переменной (константы) будет не опциональным.

var textOne = "Some text" // textOne - String
var textTwo: String? = "Some another text" // textTwo - Optional<String>

Нельзя присвоить переменной (константе) одного типа данных опциональные значения - так как это разные типы данных.

var textOne: String? = "Some text"
var textTwo: String = textOne // Error

Опциональному типу данных можно присвоить неопциональное значение. Важно, чтоб тип данных у опционального и неопционального объекта был одинаковым.

var textOne: String = "Some text"
var textTwo: String? = textOne // textTwo = Optional("Some text")

Нельзя проинициализировать переменную со значением “nil” без указания типа, так как компилятор вернет ошибку, потому что нельзя четко определить опциональный тип без контекста.

var text = nil // Error: 'nil' requires a contextual type

Таким образом, чтоб проинициализировать переменную (константу) без значения, следует ее объявить с опциональным типом данных и инициализируемым значением “nil”. После этого к ней можно обращаться и работать с ней. Например, присвоить какое-либо значение.

Внимание! Технически, константы можно делать с опциональным типом данных. Но практически, в этом нет смысла, так как изменить значение константы после инициализации нельзя. Если значение, которое следует присвоить константе, - существует, то его нет смысла делать опциональным. Если же значение не существует (nil), то присвоив его константе, позже эта константа будет занимать область памяти и не иметь значения - и изменить ее нельзя будет. То есть применение константы со значением nil должно быть обоснованным. Например, если есть необходимость добавлять в словарь элементы со значением nil

var dictionary: [Int: String?] = [1: "One", 2: "Two"]
print(dictionary)

// Output
// [1: Optional("One"), 2: Optional("Two")]

let nilValue: String? = nil
dictionary[3] = nilValue
print(dictionary)

// Output
// [1: Optional("One"), 2: Optional("Two"), 3: nil]

dictionary[4] = nilValue
print(dictionary)

// Output
// [1: Optional("One"), 2: Optional("Two"), 3: nil, 4: nil]

Примеры использования опциональных типов

Например, рассмотрим следующую ситуацию:

let someString = "123"
let someNumber = Int(someString)

Здесь инициализатор Int(someString) преобразует строку someString в число. В данном случае все нормально, так как строка "123" действительно содержит число 123. Но, что было бы в случае, если бы переменная someString представляла бы строку "hello"? В этом случае инициализатор не смог бы преобразовать строку в число. Поэтому инициализатор возвращает не просто объект “Int”, а “Int?”, то есть объект, который может иметь, а может не иметь значения.

Получение значение из опционала (разворачивание опционала)

Если обратиться к переменной (константе) с опциональным типом данных, то она вернёт значение в обертке опционала (Optional<Wrapped>).

То есть следующий пример работать не будет:

var a: Int? = 12
var b: Int = 10
var c = a + b   // ошибка - разные типы

a и b здесь переменные разных типов, хотя казалось бы обе переменных хранят целые числа. И чтобы полноценно работать с объектами опциональных типов, следует извлечь из них значение.

Например:

var a: Int? = 12
var b: Int = 10
var c = a! + b   // с = 22

Другой пример:

var b: Int = 10
var a: Int? = Int("123")
b = a! + b
print(a!)

// Output
// 123

print(b)

// Output
// 133

Разворачивание опционала - это процесс получения доступа к значению опционала путём конвертации опционального значения к неопциональному.

Для получения значения необходимо воспользоваться одним из предложенных ниже способов.

Способы разворачивания опционального типа данных

1. Принудительное разворачивание опционала (Force Unwrapping Optional)

Для получения значения применяется постфиксный оператор “!” к переменной:

var text: String? = "Some text"
var unwrapped = text! // force unwrapping

Переменной unwrapped присвоится значение переменной text.

Но если значение переменной text будет “nil”, то код не скомпилируется и вернет ошибку “Fatal error: Unexpectedly found nil while unwrapping an Optional value” (“Критическая ошибка: Неожиданное обнаружение nil во время распаковки значения опционала”).

Такой способ разворачивания опционала является небезопасным. Для его применения и для исключения ошибок следует быть уверенным, что в опционале будет значение.

2. Неявное разворачивание опционала (Implicitly Unwrapping Optional)

Swift предоставляет еще один способ получения значения подобных типов, который заключается в использовании типов Optional с неявно получаемым значением (implicitly unwrapped Optional). Как и обычные опционалы, неявно развернутые опционалы могут содержать значение или не иметь значения. Однако, в отличие от обычных необязательных параметров, вам не нужно разворачивать их, чтобы использовать: вы можете использовать их так, как будто они вообще не являются опционалами. Неявно развернутые опционалы создаются путем добавления восклицательного знака после имени типа данных:

var b: Int = 10
var a: Int! = Int("123")
b = a + b
print(a)

// Output
// Optional(123)

print(b)

// Output
// 133

Здесь переменная а имеет тип “Int!”, а не “Int?”. Фактически это тот же самый Optional, но теперь явным образом не надо применять оператор “!” для получения его значения. Хотя если отдельно вывести эту переменную в консоль, то она будет опционального типа.

Поскольку неявно развернутые опционалы ведут себя так, как будто они уже развернуты, не нужно использовать конструкции if let или guard let для разворачивания неявно развернутых опционалов. Однако, если использовать их, а они не будут иметь никакого значения — если значение nil — код выдаст ошибку.

Неявно развернутые опционалы необходимы в случаях, когда переменная начинает существовать как “nil”, но всегда будет иметь значение до того, как понадобится ее использовать. Поскольку известно, что такая переменная будет иметь значение к тому времени, когда потребуется её использовать, полезно не писать конструкции if let или guard let все время.

Еще пример:

var str1: String = "textOne"
var str2: String! = "textTwo"
var str3: String = str2
var str4 = str1 + str2
print("value: \(str1), type: \(type(of: str1))")

// Output
// value: textOne, type: String

print("value: \(str2), type: \(type(of: str2))")

// Output
// value: Optional("textTwo"), type: Optional<String>

print("value: \(str3), type: \(type(of: str3))")

// Output
// textTwo, type: String

print("value: \(str4), type: \(type(of: str4))")

// Output
// value: textOnetextTwo, type: String

В этом примере если переменной str3 явно не указать тип “String”, то ее тип будет опциональным, как и у переменной str2.

Если значение опциональной переменной будет “nil”, то код не скомпилируется и вернет ошибку “Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value” (“Критическая ошибка: Неожиданное обнаружение nil во время распаковки значения опционала”).

Такой способ разворачивания опционала является небезопасным. Для его применения и для исключения ошибок следует быть уверенным, что в опционале будет значение.

3. Простая проверка с помощью условного оператора If (If Statement)

Чтоб более безопасно принудительно развернуть опционал, можно предварительно проверить на существование значения простой конструкцией if-else:

var foo: Int? = 5
if foo != nil {
	print(foo!)
} else {
	print("no data")
}

При этом есть несколько нюансов. Во-первых, проверка на равенство/неравенство nil применима только к nullable-типам и не применима к примитивным типам, структурам и перечислениям. Для обозначения отсутствия значения у переменной примитивного типа приходится вводить спецзначения, такие, как NSNotFound.

Соответственно, пользователь этой переменной должен учитывать, что спецзначения возможны. В Swift даже примитивный тип можно использовать в опциональном стиле, т.е явным образом указывать на то, что значения может не быть.

Во-вторых: явная опциональность проверяется на этапе компиляции, что снижает количество ошибок в runtime. Опциональную переменную в Swift нельзя использовать точно так же, как неопциональную (за исключением неявно извлекаемых опционалов, подробности в разделе Implicit Unwrapping). Опционал нужно либо принудительно преобразовывать к обычному значению, либо использовать специальные преобразующие идиомы, такие как if let, guard let и ??.

В-третьих, опционалы синтаксически более лаконичны, чем проверки на nil, что особенно хорошо видно на цепочках опциональных вызовов — так называемый “Optional Chaining”.

4. Опциональное привязывание “if let” (Optional binding - if let)

Это безопасный способ извлечения опционала.

Для его применения используется конструкция if let. Далее вводиться переменная (константа) которой присваивается значение опциональной переменной (константы). Если у опциональной переменной есть значение и его можно развернуть, то это значение присваивается вводимой переменной и далее выполняется код в блоке if. Если же у опциональной переменной (константы) значение равно nil (и значит извлечь его нельзя), то выполнение кода выполняется в блоке else и далее по коду.

Общая форма:

if let константа = опциональное_значение {
    действия1
} else {
    действия2
}

Например:

var text: String? = "Some text"
if let unwrapped = text {
	print("Optional was successfully unwrapped and its value: \(unwrapped)")
} else {
	print("found nil while unwrapped")
}

Для разворачивания опционала мы инициализировали временную константу. Если же значение этой константы внутри блока if необходимо менять, то можно либо объявить переменную и присвоить ей значение этой временной константы внутри блока if, либо записать конструкцию как if var:

var str: String = "123"
var b: Int = 10
if var a = Int(str) {
    a += b
    print(a)
}
else{
    print(b)
}

Если выражение Int(str) (которое возвращает объект “Int?”) успешно преобразует строку в число, то есть будет иметь значение, то создается переменная a, которой присваивается полученное значение, и затем выполняется код:

a += b
print(a)

Если же преобразование из строки в число завершится с ошибкой, и выражение Int(str) возвратит значение “nil”, то выполняется код в блоке else:

else {
    print(b)
}

Если надо проверить значения нескольких переменных или констант, то все их можно указать в одном выражении “if”:

let a = Int("123")
let b = Int("456")
if let aVal = a, let bVal = b {
    print(aVal)
    print(bVal)
}
else {
    print("Error")
}

В данном случае выражение в блоке if выполняется, если и a, и b не равны “nil”. Иначе выполняется блок else.

5. Опциональное привязывание “guard let” (Optional binding - guard let)

Это безопасный способ извлечения опционала.

Конструкция guard statement предусматривает наличие блока else (и с оператором return). Таким образом такой способ раскрытия опционала возможен внутри функций.

Данный способ похож на метод “опционального привязывания”. Если развернуть опционал возможно, то введенная константа принимает это значение, и код выполняется дальше. Если значение опционала nil, то выполняется переход в блок else с выходом из функции:

var text: String? = "Some text"
func guardTest() {
	guard let unwrapped = text else {
		return
	}
	print(unwrapped as Any)
}

guardTest() 

// Output
// Some text

text = nil
guardTest() // ничего не произойдет

if let или guard let?

Область видимости извлеченной переменной в условии 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)

6. Оператор объединения со значением nil (Nil Coalescing Operator)

Это безопасный способ извлечения опционала.

Оператор объединения обозначается двумя вопросительными знаками (??) после опционального значения. Поле оператора указывается значение по умолчанию.

Работает эта конструкция следующим образом. Если опциональное значение можно получить, то возвращается это значение. Если опционального значения нет (nil), то возвращается значение по умолчанию.

Пример:

var textOne: String? = "Some text"
var unwrappedOne = textOne ?? "default text"
print(unwrappedOne) 

// Output
// Optional("SOME TEXT")

var textTwo: String? = nil
var unwrappedTwo = textTwo ?? "default text"
print(unwrappedTwo)

// Output
// default text

7. Опциональная цепочка (Optional chaining)

Это безопасный способ извлечения опционала.

Бывают случаи, когда необходимо получить доступ к свойствам и методам внутри опционального значения - создаётся цепочка обращений. Тогда, при обращении к опционалу в этой цепочке, следует поставить знак “?” после него.

Optional Chaining — это процесс последовательных вызовов по цепочке, где каждое из звеньев возвращает опционал. Процесс прерывается на первом опционале, находящемся в состоянии “nil” — в этом случае результатом всей цепочки вызовов также будет “nil”. Если все звенья цепочки находятся в состоянии “.some”, то результирующим значением будет опционал с результатом последнего вызова. Для формирования звеньев цепочки используется лексема ?, которая помещается сразу за вызовом, возвращающим опционал. Звеньями цепочки могут любые операции, которые возвращают опционал: обращение к локальной переменной (в качестве первого звена), вызовы свойств и методов, доступ по индексу.

“Optional сhaining” всегда работает последовательно слева направо. Каждому следующему звену передается “.some”-значение предыдущего звена, при этом результирующее значение цепочки всегда является опционалом, т.е. цепочка работает по следующим правилам:

  • первое звено должно быть опционалом;
  • после лексемы ? должно быть следующее звено;
  • если звено в состоянии “.none”, то цепочка прерывает процесс вызовов и возвращает “nil”;
  • если звено в состоянии “.some”, то цепочка отдает “.some”-значение звена на вход следующему звену (если оно есть);
  • если результат последнего звена является опционалом, то цепочка возвращает этот опционал;
  • если результат последнего звена не является опционалом, то цепочка возвращает этот результат, “завернутый” в опционал (результат вычислений присваивается “.some”-значению возвращаемого опционала).

Пример:

var textOne: String? = "Some text"
var unwrappedOne = textOne?.uppercased()
print(unwrappedOne as Any) 

// Output
// print: Optional("SOME TEXT")

var textTwo: String? = nil
var unwrappedTwo = textTwo?.uppercased()
print(unwrappedTwo as Any) 

// Output
// nil

Опциональная цепочка (optional chaining) - процесс запросов и вызовов свойств, методов, сабскриптов (индексов) у опционала, который может быть nil. Если опционал содержит какое-либо значение, то вызов свойства, метода или сабскрипта успешен, будет передано значение .some(value) и перейдет к следующей части цепочки. В случае, если значение у опционала нет, то вызов свойства, метода или сабскрипта возвращает nil. Множественные запросы могут быть соединены вместе, и вся цепочка этих запросов вернёт nil, если хотя бы один запрос равен nil.

Вы обозначаете опциональную последовательность, когда ставите вопросительный знак (?) опционального значения, свойство, метод или индекс которого вы хотите вызвать, если опционал не “nil”. Это очень похоже на установку восклицательного знака (!) после опционального значения для принудительного извлечения его значения. Основное отличие в том, что опциональная последовательность не исполняется, если опционал равен “nil”, в то время как принудительное извлечение приводит к ошибке (Runtime Error), когда опционал равен “nil”.

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

Первые два класса Person, House определены как:

class Person {
    var house: House?
}
 
class House {
    var numberOfRooms = 1
}

Экземпляры House имеют единственное свойство numberOfRooms типа Int, со значением по умолчанию 1. Экземпляры Person имеют опциональное свойство house типа “House?”.

Если создать новый экземпляр Person, то его свойство house по умолчанию имеет значение “nil”, в силу того, что оно является опционалом. В коде ниже объект bob имеет свойство house, значение которого “nil”:

let bob = Person()

Если попытаться получить доступ к свойству numberOfRooms свойства house экземпляра Person, поставив восклицательный знак после house, для принудительного извлечения, то получится ошибка исполнения (Runtime Error), потому что house не имеет значения для извлечения:

let roomCount = bob.house!.numberOfRooms // Runtime Error

Код, представленный выше, срабатывает успешно, если bob.house имеет не nil-значение и устанавливает корректное значение типа Int для roomCount. Однако этот код всегда будет выдавать ошибку исполнения, когда house равен “nil”, что указано выше.

Опциональная последовательность предоставляет альтернативный способ получить доступ к значению numberOfRooms. Для использования опциональной последовательности следует использовать вопросительный знак, на месте восклицательного знака:

if let roomCount = bob.house?.numberOfRooms {
    print("Bob's house has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}

// Output
// Unable to retrieve the number of rooms.

Этот код сообщает Swift “объединиться” с опциональным свойством house и получить значение numberOfRooms, если house существует.

Так как попытка доступа к numberOfRooms имеет потенциальную возможность претерпеть неудачу, то опциональная последовательность возвращает значение типа Int? или “опциональный Int”. Когда house равен “nil”, в примере выше, этот “опциональный Int” также будет “nil”, для отображения того факта, что было невозможно получить доступ к numberOfRooms. “Опциональный Int” получает доступ через необязательную привязку для разворачивания целого числа и присваивает неопциональное значение переменной roomCount.

Следует обратить внимание на то, что это верно даже если numberOfRooms - “неопциональный Int”. Факт того, что запрос был через опциональную последовательность означает, что вызов numberOfRooms будет всегда возвращать “Int?”, вместо “Int”.

Можно присвоить экземпляр House в bob.house, так что это свойство больше не будет со значением “nil”:

bob.house = House()

Теперь bob.house содержит экземпляр House, а не “nil”. Если попытаться получить доступ к свойству numberOfRooms с той же последовательностью опционала, что и раньше, то теперь последовательность вернет “Int?”, который содержит значение по умолчанию numberOfRooms равное 1:

if let roomCount = bob.house?.numberOfRooms {
    print("Bob's house has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}

// Output
// Bob's house has 1 room(s).

Важно отличать цепочки опциональных вызовов от вложенных опционалов. Вложенный опционал образуется, когда “.some”-значением одного опционала является другой опционал.

8. Патерн разворачивания опционала (Optional pattern)

Это безопасный способ извлечения опционала.

Данный метод похож на метод опционального привязывания. Отличия в том, что вместо конструкции if let используется if case let и после введенной переменной стоит вопросительный знак в качестве постфиксного оператора:

var text: String? = "Some text"
if case let unwrapped? = text {
	print(unwrapped) // print: Some text
}

text = nil
if case let unwrapped? = text {
	print(unwrapped) // ничего не произойдет
}

9. Сравнение объектов Optional

При сравнении объекта Optional с объектом конкретного типа, Swift преобразует объект конкретного типа к типу Optional:

let a: Int? = 10
if a == 10 {
	print(a)
    print("a is equal to 10")
}
else {
	print(a)
    print("a is not equal to 10")
}

В этом примере в консоль выведется две строки:

  • Optional(10)
  • a is equal to 10

Если константа а будет иметь значение не 10, а например 8, то в консоль выведется две строки:

  • Optional(8)
  • a is not equal to 10

Если константа а будет иметь значение nil, то в консоль выведется две строки:

  • nil
  • a is not equal to 10

И таким образом работают операции == и !=. Однако с операциями <, >, <=, >= все будет несколько иначе. Например, следующий код выдаст ошибку:

let a: Int? = 10
if a > 5 {
    print("a is greater than 5")
}

И в подобных операциях к объекту Optional необходимо применить оператор “!”:

let a: Int? = 10
if a != nil && a! > 5 {
    print("a is greater than 5")
}

10. Optional в switch…case

Если сравниваемое значение в конструкции “switch…case” представляет объект Optional, то с помощью операции “?” можно получить и сравнивать его значение при его наличии:

let i = Int("1")
switch i {
case 1?:
    print("i is equal to 1")
case let n?:
    print("i is equal to \(n)")
case nil:
    print("i is undefined")
}

В данном примере в консоль будет выведено: “i is equal to 1”.

Еще один пример использования “switch…case” в разделе “Что внутри опционала?” ниже.


Еще немного про применение опционала

Optional Binding и приведение типов

В Swift гарантированное компилятором приведение типов (например, повышающее приведение или указание типа литерала) выполняется с помощью оператора “as”. В случаях, когда компилятор не может гарантировать успешное приведение типа (например, понижающее приведение), используется либо оператор принудительного приведения “as!”, либо оператор опционального приведения “as?”. Принудительное приведение работает в стиле “Force Unwrapping”, т.е. в случае невозможности выполнить приведение приведет к ошибке (Runtime Error), в то время как опциональное приведение в этом случае вернет “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)") 

// Output
// 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!, который позволяет проигнорировать возможность выброса исключения из функции. Если исключение все же будет выброшено, то произойдет ошибка (Runtime Error):

// ошибка рантайма, если loadImage будет выброшено исключение
// photo не является опционалом (в отличие от x и y)
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

Что внутри опционала?

Опционал в Swift представляет собой особый объект-контейнер, который может содержать в себе либо “nil”, либо объект конкретного типа, который указывается при объявлении этого контейнера. Эти два состояния обозначаются терминами “None” и “Some” соответственно. Если при создании опциональной переменной не указывать значение, то, по умолчанию, будет присвоено значение “nil”.

Опционал представляет собой системное перечисление с двумя значениями:

public enum Optional<Wrapped>: ExpressibleByNilLiteral {
    // The absence of a value.
    case none

    // The presence of a value, stored as `Wrapped`.
    case some(Wrapped)
}

Перечисление 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. Этот конструктор используется компилятором.

Поскольку опциональный тип данных — это просто перечисление, можно использовать его в случае switch:

struct User {
    var age: Int?
}
var user = User()

switch user.age {
case .some(let age):
    // if user.age has a value
case .none:
    // if user.age is nil
}

Это еще один способ развернуть опционал.

Что еще можно сделать с опционалом? Написать расширение

Так как опционал - это перечисление, то к нему можно написать расширение.

Например:

extension Optional where Wrapped == String {
    var isNilOrEmpty: Bool {
        return self?.isEmpty ?? true
    }
}

Это расширение добавляет свойство isNilOrEmpty к опциональному типу “String”, которое получает булево значение: в случае, если опциональная строка пустая или имеет значение “nil” - свойство isNilOrEmpty будет иметь значение true

Теперь мы можем использовать вычисляемое свойство isNullOrEmpty для каждой опциональной строки:

var name: String?
if name.isNilOrEmpty {
	print("Property name is empty or has a value 'nil'")
}

Еще полезные ссылки

Также информацию по опциональным типам данных можно получить на странице официальной документации.

Ссылки на официальную документацию: