Когда одного имени переменной недостаточно
Когда вы работаете с иерархией классов, переменная часто хранит ссылку на базовый тип, а по факту в ней лежит объект какого-то из наследников. Представьте посылку с надписью Животное: внутри может оказаться и собака, и кошка, но по одной только надписи на коробке этого не узнать. То же самое происходит и с массивом, в который специально собрали значения разных, никак не связанных типов — числа, строки, что угодно. Чтобы обращаться с содержимым правильно, нужен способ на месте спросить «а что тут на самом деле» и, если нужно, достать значение уже с точным типом.
Официальные термины: is, as?, as!, as
В Swift за это отвечают четыре ключевых слова.
is— проверка типа, возвращаетBool: подходит значение под указанный тип или нет.as?— условное приведение (downcast): пытается привести значение к более узкому типу и возвращаетOptional— либо приведённое значение, либоnil, если не получилось.as!— принудительное приведение: ведёт себя какas?, но сразу разворачивает результат. Если приведение не удалось, программа падает с ошибкой времени выполнения.as— восходящее приведение (upcast) к более широкому типу или явное указание типа выражения. Оно всегда безопасно и проверяется компилятором ещё на этапе сборки, потому что подкласс всегда «является» своим родителем.
Проверка типа: is
Самый безопасный вариант — просто спросить, подходит ли значение под тип, без попытки его достать.
class Animal {}
class Dog: Animal {}
class Cat: Animal {}
let pet: Animal = Cat()
if pet is Cat {
print("Это кошка")
}
Здесь pet объявлена с типом Animal, но фактически хранит экземпляр Cat. Оператор is проверяет реальный тип значения во время выполнения и возвращает true. Сам объект при этом никуда не приводится — вы получаете только ответ да/нет.
Условное приведение: as?
Если помимо факта совпадения типа вам нужен ещё и сам объект уже с точным типом, используйте as?.
let pet: Animal = Cat()
if let cat = pet as? Cat {
print("Достали кошку: \(cat)")
} else {
print("Это не кошка")
}
pet as? Cat возвращает Optional<Cat>. Если приведение удалось, if let разворачивает его и внутри блока cat уже имеет тип Cat — со всеми его собственными свойствами и методами, если бы они были. Если бы в pet лежала собака, выражение вернуло бы nil, и выполнилась бы ветка else.
Принудительное приведение: as!
as! стоит использовать только тогда, когда вы абсолютно уверены в реальном типе значения — например, только что сами его туда положили.
let pet: Animal = Cat()
let cat = pet as! Cat
print(cat)
Этот код отработает нормально, потому что pet действительно хранит Cat. Но если бы там оказалась собака, строка с as! завершила бы программу крахом на месте, без шанса на обработку — в отличие от as?, здесь нет возможности мягко откатиться назад.
Восходящее приведение: as
Обычный as без вопросительного или восклицательного знака нужен не для проверки, а чтобы явно указать компилятору, каким типом должно быть выражение. Частый случай — литерал массива из разнородных значений.
let mixed = [1, "два", 3] as [Any]
print(mixed.count)
Если элементы литерала массива относятся к разным типам, компилятор не может сам решить, какой общий тип у массива, и потребует явной подсказки. as [Any] — как раз такая подсказка: «сделай из этого массив, где каждый элемент имеет тип Any».
Any и AnyObject
Any — это тип, который может представлять значение вообще любого типа: число, строку, структуру, перечисление, класс, даже функцию. AnyObject — более узкий тип, он подходит только для экземпляров классов, то есть для ссылочных типов. Массив [Any] уместен, когда среди значений могут быть и структуры вроде Int и String; массив [AnyObject] — когда вы точно знаете, что там только объекты классов, например разные наследники одной иерархии.
С массивом [Any] удобно работать через switch с привязкой значения к типу:
let items: [Any] = [1, "hello", 3.5]
for item in items {
switch item {
case let n as Int:
print("Число: \(n)")
case let s as String:
print("Строка: \(s)")
default:
print("Что-то ещё")
}
}
Конструкция case let n as Int одновременно проверяет тип и, если он совпал, привязывает значение к константе n уже нужного типа — внутри этой ветки с ним можно работать как с обычным Int. Ветка default здесь обязательна: компилятор не может доказать, что вы перечислили все возможные типы, которые в принципе могут оказаться в Any.
Частые ошибки
- Использовать
as!там, где тип заранее не гарантирован. Один непредвиденный элемент — и всё приложение падает вместо того, чтобы аккуратно обработать несовпадение черезas?. - Путать направление приведения:
as— это подъём вверх по иерархии или явная разметка типа, а не попытка достать более узкий тип. Для сужения нужныas?илиas!. - Забывать ветку
defaultвswitchпо значению типаAny— без неё код просто не скомпилируется, потому что перечислить все типы заранее нельзя. - Пытаться применить
AnyObjectтам, где на самом деле нуженAny: значения структур и перечислений не являются экземплярами классов, и массив, объявленный как[AnyObject], для них не подходит. - Строить логику на бесконечных проверках
isи приведениях там, где задачу проще и надёжнее решить без разнородного массива вообще — например, храня данные в одном общем типе с самого начала.
Резюме
isпроверяет тип значения и возвращаетBool.as?пытается привести значение к более узкому типу и возвращаетOptional.as!делает то же самое, но сразу разворачивает результат и падает крахом при неудаче.as— безопасный подъём к более широкому типу или явное указание типа выражения, проверяется на этапе компиляции.Anyподходит для значений любого типа,AnyObject— только для экземпляров классов.- В
switchпо значению типаAnyудобно использоватьcase let x as Тип, но веткаdefaultобязательна.
Проверьте себя
4 вопроса
Посчитайте типы в разнородном массиве
Напишите функцию classify, которая принимает массив [Any] и возвращает массив из трёх чисел: сколько в исходном массиве элементов типа Int, сколько типа String, и сколько всех остальных (например, Double или Bool).
Результат — массив вида [количество Int, количество String, количество прочих]. Проверяйте тип каждого элемента оператором is. Порядок проверки важен: сначала проверяйте Int, затем String, всё остальное считайте «прочим».
Посчитайте кошек среди животных
Дана простая иерархия классов:
class Animal {}
class Dog: Animal {}
class Cat: Animal {}
Напишите функцию catCount, которая принимает массив [AnyObject], где вперемешку лежат экземпляры Dog и Cat, и возвращает количество элементов, являющихся Cat. Используйте оператор is для проверки типа каждого элемента.