Основы Swift / 15.1. Опциональная цепочка


Видео


Опциональная цепочка
Опицональная цепочка - это способ запроса и вызова свойств, методов и сабскриптов на онционале, который может быть в действительности nil. Если опционал содержит значение, то свойство, метод или сабскрипт будет вызван успешно; если опционал равен nil, то свойство, метод или сабскрипт вернут nil. Множественные запросы могут быть сцеплены вместе, так что провал всего запроса может быть вызван, если любая ссылка в цепочке будет nil.

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

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

Результат вызова опциональной цепочки имеет тот же тип, что и ожидаемое возвращаемое значение, но оборачивается в опционал. Свойство, обычно возвращающее Int, вернёт Int? при доступе через опциональную цепочку.

Следующие несколько примеров кода демонстрируют, как опциональная цепочка отличается от принудительной распаковки и позволяет Вам проверить успех.
@{15.1\1\1}
Во-первых, определим два класса с названиями Person и Residence:
class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}
Объект Residence имеет единственное свойство типа Int с названием numberOfRooms и значением по-умолчанию 1. Объекты Person имеют опциональное свойство residence типа Residence?.
@{15.1\1\2}
Если Вы создаёте новый объект типа Person, то его свойство residence инициализировано в nil ввиду его опциональности. В коде ниже john имеет значение свойства residence nil:
let john = Person()
@{15.1\1\3}
Если Вы попытаетесь получить доступ к свойству numberOfRooms объект residence у этого человека, поставив восклицательный знак после residence для принудительной распаковки его значения, то Вы получите ошибку времени выполнения, так как нет значения в residence для распаковки:
let roomCount = john.residence!.numberOfRooms
// это вызовет ошибку времени выполненияs
Код выше будет выполнен верно, когда john.residence имеет не-nil значение и будет устанавливать roomCount в Int-значение, содержащее подходящее количество комнат. Однако этот код всегда будет вызывать рантайм ошибку, когда residence имеет nil, как показано выше.
@{15.1\1\4}
Опциональная цепочка предоставляет альтернативный способ для доступа к значению 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.
@{15.1\1\5}
Вы можете присвоить объект Residence свойству john.residence, так что больше не будет в нём значения nil:
john.residence = Residence()
@{15.1\1\6}
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)."
Определение моделей классов для опциональной цепочки
Вы можете использовать опциональную цепочку с вызовами для свойств, методов и сабскриптов, которые находятся на глубине больше 1 уровня. Это позволяет Вам проникать вниз в подсвойства внутри комплексных моделей связанных типов и проверять, возможно ли получить доступ к свойствам, методам и сабскриптам этих подсвойств.

Примеры кода ниже определяют четыре модели-класса для использования в нескольких последующих примерах, включая примеры многоуровневого опционального связывания. Эти классы расширяют модели Person и Residence из примеров выше путём добавления классов Room и Address со связанными свойствами, методами и сабскриптами.
@{15.1\2\1}
Класс Person определён так же, как и ранее:
class Person {
    var residence: Residence?
}
@[15.1\2\2}
Класс 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("The number of rooms is \(numberOfRooms)")
    }
    var address: Address?
}
Так как эта версия Residence хранит массив типа Room, то его свойство numberOfRooms реализуется как вычисляемое свойство, а не хранимое. Вычисляемое свойство numberOfRooms просто возвращает значение свойства count массива rooms.

В качестве сокращённого способа доступа к его массиву rooms эта версия Residence предоставляет сабскрипт чтения-записи, который предоставляет доступ к комнате по запрашиваемому индексу в массиве rooms.

Эта версия Residence также предоставляет метод с названием printNumberOfRooms, который просто выводит количество комнат в резиденции.

Помимо этого Residence определяет опциональное свойство с названием address и типом Address?. Класс Address для этого типа определён ниже.
@{15.1\2\3}
Класс Room, используемый для массива rooms, это простой класс с одним свойством name и конструктором для задания свойства с подходящим именем комнаты:
class Room {
    let name: String
    init(name: String) { self.name = name }
}
@{15.1\2\4}
Последний класс в этой модели называется 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, сцепленный с street, если оба имеют значение, или nil в противном случае.
Получение доступа к свойствам через опциональную цепочку
Вы можете использовать опциональную цепочку для доступа к свойству на опциональном значении, если доступ к этому свойству успешен.

Используйте классы, определённые выше, для создания нового объекта Person и для попытки получить numberOfRooms как раньше:
@{15.1\2\5}
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, эта опциональная цепочка провалится, как и раньше.
@{15.1\2\6}
Вы также можете попытаться установить значение свойства через опциональную цепочку:
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress
В этом примере попытка установить значение address у john.residence провалится, так как john.residence равен nil.

Это присваивание есть часть опциональной цепочки, что значит, что никакой код по правую сторону от оператора = не будет выполнен. В предыдущем примере непросто увидеть, что someAddress никогда не будет вычислен, так как доступ к константе не имеет никаких побочных эффектов. Листинг ниже желает то же присваивание, но он использует функцию для создания адреса. Эта функция выводит "Function was called" перед возвращением значения, что позволяет Вам увидеть, вычисляется ли правая часть оператора =.
@{15.1\2\7}
Вы можете сказать, что функция createAddress() не вызывается, потому что ничего не выводится.
func createAddress() -> Address {
    print("Function was called.")

    let someAddress = Address()
    someAddress.buildingNumber = "29"
    someAddress.street = "Acacia Road"

    return someAddress
}
john.residence?.address = createAddress()
Вызов методов через опциональную цепочку
Вы можете использовать опциональную цепочку для вызова метода на опциональном значении и для проверки того, что этот вызов был успешен. Вы можете сделать это, даже если этот метод не определяет возвращаемое значение.
@{15.1\2\8}
Метод printNumberOfRooms() класса Residence выводит текущее значение numberOfRooms. Вот так выглядит тот метод:
func printNumberOfRooms() {
    print("The number of rooms is \(numberOfRooms)")
}
Этот метод не определяет возвращаемый тип. Однако функции и методы без возвращаемого значения имеют неявное значение Void. Это означает, что они возвращают значение () или пустой кортеж.
@{15.1\2\9}
Если Вы вызовите этот метод на опциональном значении с опциональной цепочкой, то возвращаемый тип метода метода будет Void?, а не Void, так как возвращаемые значения всегда имеют опциональный тип, когда вызываются через опциональную цепочку. Это позволяет Вам использовать инструкцию if для проверки того, возможно ли вызвать метод printNumberOfRooms(), хотя он и не определяет возвращаемое значение. Сравните возвращаемое значение у printNumberOfRooms c nil, чтобы увидеть, если метод был успешно вызван:
if john.residence?.printNumberOfRooms() != nil {
    print("It was possible to print the number of rooms.")
} else {
    print("It was not possible to print the number of rooms.")
}
// Выведет "It was not possible to print the number of rooms."
@{15.1\2\10}
То же самое верно, если попытаться установить свойство через опциональную цепочку. Пример выше в {Получении доступа к свойствам через опциональную цепочку} устанавливает значение address в john.residence, даже хотя свойство residence равен nil. Любая попытка задать свойство через опциональную цепочку возвращает Void?, что позволяет Вам сравнить его против nil, чтобы увидеть, если свойство было успешно задано:
if (john.residence?.address = someAddress) != nil {
    print("It was possible to set the address.")
} else {
    print("It was not possible to set the address.")
}
// Выведет "It was not possible to set the address."
Доступ к сабскриптам через опциональную цепочку
Вы можете использовать опциональную цепочку для попытки доступа для извлечения и установки значения из сабскрипта опционального значения и для проверки того, был ли вызов сабскрипта успешен.

Когда Вы получаете доступ к санскриту на опциональном значении через опциональную цепочку, Вы размещаете знак вопроса перед квадратными скобками сабскриптов, а не после. Знак вопроса опциональной цепочки всегда следует сразу после той части выражения, которая является опциональной.
@{15.1\2\11}
Пример ниже пытается получить имя для первой комнаты в массиве rooms у свойства john.residence, используя сабскрипт, определённый на классе Residence. Так как john.residence установлен в nil, то вызов сабскрипта провалится:
if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
} else {
    print("Unable to retrieve the first room name.")
}
// Выведет "Unable to retrieve the first room name."
Знак вопроса опциональной цепочки в этом вызове сабскрипта размещён сразу после john.residence перед квадратными скобками, так как john.residence - это опциональное значение, на котором происходит вызов опциональной цепочки.
@{15.1\2\12}
Аналогично Вы можете попытаться задать новое значения сабскрипта с помощью опциональной цепочки:
john.residence?[0] = Room(name: "Bathroom")
Эта установка сабскрипта также провалится, так как residence сейчас равен nil.
@{15.1\2\13}
Если Вы создадите и присвоите настоящее значение Residence свойству john.residence с одним или более объектами типа Room в массиве rooms, то Вы сможете использовать сабскрипт Residence для доступа к актуальным элементам в массиве rooms через опциональную цепочку:
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Living Room"))
johnsHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
} else {
    print("Unable to retrieve the first room name.")
}
// Выведет "The first room name is Living Room."
Доступ к сабскриптам на опциональном типе
Если сабскрипт вернёт значение опционального типа - как например сабскрипт для ключа в тип Dictionary - разместите знак вопроса после закрывающей квадратной скобки сабскрипта для установки цепочки через его опциональное возвращаемое значение:
@{15.1\3}
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 c двумя парами ключ-значение, отображающих ключ String в массив [Int]. Этот пример использует опциональную последовательность для задания первого элемента "Dave" в массиве в значение 91; для увеличения первого элемента "Bev" в массиве 1; а также попытки установить первый элемент в массиве "Brian". Первые два вызова будут успешными, так как словарь testScores содержит ключи "Dave" и "Bev". Третий вызов провалится, так как словарь testScores не содержит ключа "Brian".
Сцепление нескольких уровней связывания
Вы можете сцепить вместе несколько уровней опционального связывания для спуска вниз по свойствам, методам и санскритам вглубь модели. Однако множественные уровни опционального связывания не добавляют больше уровней опциональности возвращаемую значению.

Другими словами:
  • Если тип, который Вы пытаетесь извлечь, неопционален, то он станет опциональным ввиду опционального связывания.
  • Если тип, который Вы пытаетесь извлечь, уже опционален, то он не станет более опциональным ввиду опционального связывания.
Следовательно:
  • Если Вы попытаетесь извлечь значение @code(Int) через опциональную цепочку, то всегда будет возвращено @code(Int?), не важно сколько уровней цепочки использовано.
  • Аналогично, если Вы попытаетесь извлечь значение @code(Int?) через опциональную цепочку, то всегда будет возвращена значение @code(Int?), не важно сколько уровней цепочки использовано.
@{15.1\2\14}
Пример ниже пытается получить доступ к свойству 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?, хотя два уровня опциональной цепочки применяются в дополнение к подразумевающемуся опциональному типу свойства.
@{15.1\2\15}
Если Вы зададите текущее значение 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 теперь содержит корректный объект Residence.
Опциональные цепочки на методах с опциональными возвращаемыми значениями
Предыдущий пример показывает, как извлечь значение свойства опционального типа через опциональную цепочку. Вы также можете использовать опциональную цепочку для вызова метода, возвращающего опциональный тип, и продолжить её на возвращаемом значении метода, если нужно.

Пример ниже демонстрирует вызов метода buildingIdentifier() класса Address через опциональную цепочку. Этот метод возвращает значение типа String?. Как описано выше, абсолютный возвращаемый тип вызова этого метода после опциональной цепочки также равен String?:
@{15.1\2\16}
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
    print("John's building identifier is \(buildingIdentifier).")
}
// Выведет "John's building identifier is The Larches."
@{15.1\2\17}
Если Вы хотите произвести дальнейшее опциональное сцепление на возвращаемом значении этого метода, разместите опциональный знак вопроса после круглых скобок метода:
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(), а не сам этот метод.