Опциональная последовательность
Опциональная цепочка (optional chaining) - процесс запросов и вызовов свойств, методов, сабскриптов (индексов) у опционала, который может быть nil. Если опционал содержит какое-либо значение, то вызов свойства, метода или сабскрипта успешен, и наоборот, если опционал равен nil, то вызов свойства, метода или сабскрипта возвращает nil. Множественные запросы могут быть соединены вместе, и вся цепочка этих запросов не срабатывает, если хотя бы один запрос равен nil.
Заметка
Опциональная последовательность в Swift - аналог сообщению nil в Objective-C, но только она работает со всеми типами, и может быть проверена на успех или неудачу.
ОП как альтернатива принудительному извлечению
Вы обозначаете опциональную последовательность, когда ставите вопросительный знак (?) опционального значения, свойство, метод или индекс которого вы хотите вызвать, если опционал не nil. Это очень похоже на установку восклицательного знака (!) после опционального значения для принудительного извлечения его значения. Основное отличие в том, что опциональная последовательность не исполняется, если опционал равен nil, в то время как принудительное извлечение приводит к runtime ошибке, когда опционал равен nil.
Факт того, что опциональная последовательность может быть вызвана и на значение nil, отражается в том, что результатом работы опциональной последовательности всегда является опциональная величина, даже в том случае, если свойство, метод или сабскрипт, к которым вы обращаетесь, возвращает неопциональное значение. Вы можете использовать это значение опционального возврата для проверки успеха (если возвращенный опционал содержит значение) или неудачи (если возвращенное значение опционала nil).
В частности, результат вызова опциональной последовательности того же типа, что и тип ожидаемого возвращаемого значения, только в завернутом в опционал виде. Свойство, которое обычно возвращало Int, вернет Int?, когда обращаются к нему через опциональную последовательность.
Следующие несколько фрагментов кода покажут вам как отличаются опциональная последовательность от принудительного извлечения, и как она позволяет вам проверить значение на успех.
Первые два класса Person, Residence определены как:
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
Экземпляры Residence имеют единственное свойство numberOfRooms типа Int, со значением по умолчанию 1. Экземпляры Person имеют опциональное свойство residence типа Residence?.
Если вы создаете новый экземпляр Person, то его свойство residence по умолчанию имеет значение nil, в силу того, что оно является опционалом. В коде ниже john имеет свойство residence, значение которого nil:
let john = Person()
Если вы попытаетесь получить доступ к свойству numberOfRooms свойства residence экземпляра Person, поставив восклицательный знак после residence, для принудительного извлечения, то вы получите ошибку исполнения, потому что residence не имеет значения для извлечения:
let roomCount = john.residence!.numberOfRooms
// ошибка runtime
Код, представленный выше, срабатывает успешно, если john.residence имеет не nil значение и устанавливает корректное значение типа Int для roomCount. Однако этот код всегда будет выдавать ошибку исполнения, когда residence равен nil, что указано выше.
Опциональная последовательность предоставляет альтернативный способ получить доступ к значению numberOfRooms. Для использования опциональной последовательности используйте вопросительный знак, на месте восклицательного знака:
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// Выведет "Unable to retrieve the number of rooms."
Это сообщает Swift “сцепиться” с опциональным свойством residence и получить значение numberOfRooms, если residence существует.
Так как попытка доступа к numberOfRooms имеет потенциальную возможность претерпеть неудачу, то опциональная последовательность возвращает значение типа Int? или “опциональный Int”. Когда residence равен nil, в примере выше, этот опциональный Int также будет nil, для отображения того факта, что было невозможно получить доступ к numberOfRooms. Необязательный Int получает доступ через необязательную привязку для разворачивания целого числа и присваивает неопциональное значение переменной roomCount.
Обратите внимание, что это верно даже если numberOfRooms неопциональный Int. Факт того, что запрос был через опциональную последовательность означает, что вызов numberOfRooms будет всегда возвращать Int?, вместо Int.
Вы можете присвоить экземпляр Residence в john.residence, так что оно больше не будет являться значением nil.
john.residence = Residence()
Теперь john.residence содержит экземпляр Residence, а не nil. Если вы попытаетесь получить доступ к numberOfRooms с той же последовательностью опционала, что и раньше, то теперь она вернет нам Int?, который содержит значение по умолчанию numberOfRooms равное 1:
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// Выведет "John's residence has 1 room(s)."
Определение классовых моделей для ОП
Вы можете использовать опциональную последовательность для вызовов свойств, методов, сабскриптов, которые находятся более чем на один уровень глубже. Это позволяет вам пробираться через подсвойства, внутри сложных моделей вложенных типов, и проверять возможность доступа свойств, методов и сабскриптов этих подсвойств.
Фрагмент кода ниже определяет четыре модели классов для использования в нескольких следующих примерах, включая примеры с многоуровневой опциональной последовательностью. Эти классы расширяют модели Person, Residence приведенные ранее, добавляя классы Room, Address со свойствами, методами и сабскриптами.
Класс Person объявляется так же как и раньше:
class Person {
var residence: Residence?
}
Класс Residence стал намного сложнее, чем был раньше. В этот раз класс Residence определяет переменное свойство rooms, которое инициализировано пустым массивом [Room]:
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
get {
return rooms[i]
}
set {
rooms[i] = newValue
}
}
func printNumberOfRooms() {
print("Общее количество комнат равно \(numberOfRooms)")
}
var address: Address?
}
Из-за того, что эта версия Residence хранит массив экземпляров Room, его свойство numberOfRooms реализовано как вычисляемое, а не как хранимое свойство. Вычисляемое свойство numberOfRooms просто возвращает значение свойства.
В качестве сокращенного варианта доступа к массиву rooms, эта версия класса Residence предлагает сабскрипт (доступный как для чтения, так и для записи), который предоставляет доступ к комнате под требуемым индексом в массиве rooms.
Эта версия Residence так же обеспечивает метод printNumberOfRooms, который просто выводит на экран количество комнат в резиденции.
И наконец, Residence определяет опциональное свойство address типа Address?. Тип класса Address для этого свойства определен ниже.
Класс Room используется для массива rooms, в качестве простого класса с одним свойством name и инициализатором, в котором устанавливается значение свойства name как подходящее имя комнаты:
class Room {
let name: String
init(name: String) {
self.name = name
}
}
Последний класс в этой модели Address. Этот класс имеет три опциональных свойства типа String?. Первые два свойства buildingName, buildingNumber являются альтернативным вариантом определения конкретного здания как части адреса. Третье свойство street используется для названия улицы, для этого адреса:
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if let buildingNumber = buildingNumber, let street = street {
return "\(buildingNumber) \(street)"
} else if buildingName != nil {
return buildingName
} else {
return nil
}
}
}
Класс Address так же предоставляет метод buildingIdentifier(), который возвращает String?. Этот метод проверяет значения свойств экземпляра и возвращает buildingName, если у него есть значение или возвращает buildingNumber, если у него есть значение или nil, если ни у одного из свойств нет значения.
Доступ к свойствам через ОП
Как было показано в разделе ОП как альтернатива принудительному извлечению, вы можете использовать опциональную последовательность для доступа к свойству опционального значения и проверить результат доступа к этому свойству на успешность.
Используйте классы, определенные ранее, для создания нового экземпляра Person и попробуйте получить доступ к свойству numberOfRooms, как вы делали ранее:
let john = Person()
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// Выведет "Unable to retrieve the number of rooms."
Так как john.residence равен nil, этот вызов опциональной последовательности не будет успешен как и ранее.
Вы можете попробовать установить значение свойства через опциональную последовательность:
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress
В этом примере попытка установить значение свойству address опциональному свойству residence? провалится, так как john.residence все еще nil.
Присваивание является частью опциональной цепочки, что означает, что никакой код с правой стороны не вычисляется. В предыдущем примере не так просто заметить, что someAddress никогда не вычисляется, потому что доступ к самой константе не имеет никаких побочных эффектов. Пример ниже делает тоже самое присваивание, но он использует функцию для того, чтобы создать адрес. Функция выводит "Function was called" до того, как вернется значение, что позволяет вам увидеть была ли вычислена правая часть от оператора присваивания.
func createAddress() -> Address {
print("Function was called.")
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
return someAddress
}
john.residence?.address = createAddress()
Можно заметить, что функция createAddress() не вызывается, так как ничего в консоли у нас не выводится.
Вызов методов через ОП
Вы можете использовать опциональную последовательность для вызова метода опциональной величины, и проверить сам вызов метода на успешность. Вы можете сделать это, даже если этот метод не возвращает значения.
Метод printNumberOfRooms класса Residence выводит текущее значение numberOfRooms. Вот как выглядит этот метод:
func printNumberOfRooms() {
print("Общее количество комнат равно \(numberOfRooms)")
}
Этот метод не определяет возвращаемого значения. Однако функции и методы без возвращаемого значения имеют неявный возвращаемый тип Void, как было описано в главе “Функции, не возвращающие значения”. Это означает, что они возвращают значение () или просто пустой кортеж.
Если вы вызовите этот метод на опциональном значении в опциональной последовательности, то он вернет тип не Void, а Void?, потому что возвращаемые значения всегда опционального типа, когда они вызываются через опциональную последовательность. Это позволяет вам использовать конструкцию if для проверки на возможность вызова метода printNumberOfRooms, даже если метод сам не определяет возвращаемого значения. Сравните возвращаемое значение от вызова printNumberOfRooms и nil, для того чтобы увидеть, что вызов метода прошел успешно:
if john.residence?.printNumberOfRooms() != nil {
print("Есть возможность вывести общее количество комнат.")
} else {
print("Нет возможности вывести общее количество комнат.")
}
// Выведет "Нет возможности вывести общее количество комнат."
То же самое верно, если вы попытаетесь установить свойство через опциональную последовательность. Пример ниже в “Доступ к свойствам через опциональную последовательность” пытается установить значение address в john.residence, хотя свойство residence равно nil. Любая попытка установить свойство через опциональную последовательность возвращает значение Void?, которое позволяет вам сравнивать его с nil, для того, чтобы увидеть логический результат установки значения свойству (успех, провал):
if (john.residence?.address = someAddress) != nil {
print("Была возможность установить адрес.")
} else {
print("Не было возможности установить адрес.")
}
// Выведет "Не было возможности установить адрес."
Доступ к сабскриптам через ОП
Вы можете использовать опциональную последовательность для того, чтобы попробовать получить и установить значения из индекса опционального значения, и проверить успешность выполнения вызова сабскрипта.
Заметка
Когда вы получаете доступ к опциональному значению через опциональную последовательность, вы размещаете вопросительный знак до скобок сабскрипта (индекса), а не после. Вопросительный знак опциональной последовательности следует сразу после части выражения, которая является опционалом.
Пример ниже пробует получить значение имени первой комнаты в массиве rooms свойства john.residence, используя сабскрипт, определенный в классе Residence. Из-за того, что john.residence является nil, то вызов сабскрипта проваливается:
if let firstRoomName = john.residence?[0].name {
print("Название первой комнаты \(firstRoomName).")
} else {
print("Никак не получить название первой комнаты.")
}
// Выведет "Никак не получить название первой комнаты."
Вызов вопросительного знака опциональной последовательности в этом сабскрипте идет сразу после john.residence, но до скобок сабскрипта, потому что john.residence является опциональным значением, на которое применяется опциональная последовательность.
Аналогично вы можете попробовать установить новое значение через сабскрипт с помощью опциональной последовательности:
john.residence?[0] = Room(name: "Bathroom")
Это попытка установки значения через сабскрипт так же не срабатывает, так как residence все еще nil.
Если вы создадите и присвоите действительное значение экземпляру Residence, при помощи одного или нескольких экземпляров Room в массиве rooms, то вы сможете использовать сабскрипт на экземпляре residence для того, чтобы получить доступ к массиву rooms через опциональную последовательность:
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Гостиная"))
johnsHouse.rooms.append(Room(name: "Кухня"))
john.residence = johnsHouse
if let firstRoomName = john.residence?[0].name {
print("Название первой комнаты \(firstRoomName).")
} else {
print("Никак не получить название первой комнаты.")
}
// Выведет "Название первой комнаты Гостиная."
Получение доступа к сабскрипту (индексу) опционального типа
Если сабскрипт возвращает значение опционального типа, например ключ словаря типа Dictionary в Swift, то мы должны поставить вопросительный знак после закрывающей скобки сабскрипта, для присоединения его опционального возвращаемого значения:
var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72
// массив "Dave" теперь имеет вид [91, 82, 84], массив "Bev" - [80, 94, 81]
Пример выше определяет словарь testScores, который содержит две пары ключ/значение, которые соединяют ключ типа String со значением типа [Int]. Пример использует опциональную последовательность для установки значения первого элемента ключа "Dave" равным 91, для увеличения первого элемента массива под ключом "Bev" на 1 и для попытки установить первое значение несуществующего массива, соответствующего ключу "Brian" равным 72. Первые два вызова завершились успешно, потому что их ключи находятся в testScores. Третий вызов завершился неудачей, так как такого ключа как "Brian" в словаре не оказалось.
Соединение нескольких уровней ОП
Вы можете соединить несколько уровней опциональных последовательностей вместе для того, чтобы пробраться до свойств, методов, сабскриптов, которые находятся глубже в модели. Однако многоуровневые опциональные последовательности не добавляют новых уровней опциональности к возвращаемым значениям:
Скажем другими словами:
- Если тип, который вы пытаетесь получить не опциональный, то он станет опциональным из-за опциональной последовательности.
- Если тип, который вы пытаетесь получить, уже опциональный, то более опциональным он уже не станет, даже по причине опциональной последовательности.
Таким образом:
- Если вы пытаетесь получить значение типа Int через опциональную последовательность, то получите Int?, и это не будет зависеть от того, сколько уровней в опциональной последовательности задействовано.
- Аналогично, если вы попытаетесь получить значение типа Int? через опциональную последовательность, то вы получите Int?, что опять таки не зависит от количества уровней, которые задействованы в опциональной последовательности.
Пример ниже пробует получить доступ к свойству street свойства address свойства residence экземпляра john. Здесь задействовано два уровня опциональной последовательности для того, чтобы соединить свойства residence и address, оба из которых опционального типа:
if let johnsStreet = john.residence?.address?.street {
print("John's street name is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
// Выведет "Unable to retrieve the address."
Значение john.residence на данный момент содержит корректный экземпляр класса Residence. Однако значение john.residence.address равно nil. Из-за этого вызов john.residence?.address?.street проваливается.
Обратите внимание, что в примере выше вы пытаетесь получить значение свойства street. Тип этого свойства String?. Возвращаемое значение john.residence?.address?.street так же String?, даже если два уровня опциональной последовательности применены в дополнение к опциональному типу самого свойства.
Если вы установите фактический экземпляр класса Address как значение для john.residence.address и установите фактическое значение для свойства street, то вы можете получить доступ к значению свойства street через многоуровневую опциональную последовательность (цепочку):
let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence?.address = johnsAddress
if let johnsStreet = john.residence?.address?.street {
print("John's street name is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
// Выведет "John's street name is Laurel Street."
В этом примере попытка установить свойство address свойства john.residence будет успешной, потому что значение john.residence в настоящее время содержит действующий экземпляр Address.
Связывание методов в ОП с опциональными возвращаемыми значениями
Предыдущий пример показал, как можно получить значение свойства опционального типа через опциональную последовательность. Вы так же можете использовать опциональную последовательность для вызова метода, который возвращает значение опционального типа, а затем к этой опциональной последовательности может прикрепить и возвращаемое значение самого метода, если это нужно.
Пример ниже вызывает метод buildingIdentifier класса Address через опциональную последовательность. Этот метод возвращает значение типа String?. Как было описано ранее, что возвращаемый тип этого метода после опциональной последовательности так же будет String?:
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
print("John's building identifier is \(buildingIdentifier).")
}
// Выведет "John's building identifier is The Larches."
Если вы хотите продолжить свою опциональную привязку и на возвращаемое значение метода, то разместите вопросительный знак после круглых скобок самого метода:
if let beginsWithThe =
john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
if beginsWithThe {
print("John's building identifier begins with \"The\".")
} else {
print("John's building identifier does not begin with \"The\".")
}
}
// Выведет "John's building identifier begins with "The"."
Заметка
В примере выше вы разместили вопросительный знак опциональной привязки после круглых скобок метода, потому что опциональная величина, которую вы присоединяете к последовательности, является возвращаемой величиной метода buildingIdentifier, а не самим методом buildingIdentifier.