$ sudo teach IT

Когда одного имени переменной недостаточно

Когда вы работаете с иерархией классов, переменная часто хранит ссылку на базовый тип, а по факту в ней лежит объект какого-то из наследников. Представьте посылку с надписью Животное: внутри может оказаться и собака, и кошка, но по одной только надписи на коробке этого не узнать. То же самое происходит и с массивом, в который специально собрали значения разных, никак не связанных типов — числа, строки, что угодно. Чтобы обращаться с содержимым правильно, нужен способ на месте спросить «а что тут на самом деле» и, если нужно, достать значение уже с точным типом.

Официальные термины: 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 для проверки типа каждого элемента.