Приведение и проверка типов

Операторы is и !is

Мы можем проверить принадлежит ли объект к какому-либо типу во время исполнения с помощью оператора is или его отрицания !is:

if (obj is String) {
    print(obj.length)
}

if (obj !is String) { // то же самое, что и !(obj is String)
    print("Not a String")
}
else {
    print(obj.length)
}

Умные приведения

Во многих случаях в Kotlin вам не нужно использовать явные приведения, потому что компилятор следит за is-проверками для неизменяемых значений и вставляет приведения автоматически, там, где они нужны:

fun demo(x: Any) {
    if (x is String) {
        print(x.length) // x автоматически преобразовывается в String
    }
}

Компилятор достаточно умён для того, чтобы делать автоматические приведения в случаях, когда проверка на несоответствие типу (!is) приводит к выходу из функции:

    if (x !is String) return
    print(x.length) // x автоматически преобразовывается в String

или в случаях, когда приводимая переменная находится справа от оператора && или ||:

    // x автоматически преобразовывается в String справа от `||`
    if (x !is String || x.length == 0) return

    // x автоматически преобразовывается в String справа от `&&`
    if (x is String && x.length > 0) {
        print(x.length) // x автоматически преобразовывается в String
    }

Такие умные приведения работают вместе с when-выражениями и циклами while:

when (x) {
    is Int -> print(x + 1)
    is String -> print(x.length + 1)
    is IntArray -> print(x.sum())
}

Заметьте, что умные приведения не работают, когда компилятор не может гарантировать, что переменная не изменится между проверкой и использованием. Более конкретно, умные приведения будут работать:

  • с локальными val переменными - всегда;
  • с val свойствами - если поле имеет модификатор доступа private или internal, или проверка происходит в том же модуле, в котором объявлено это свойство. Умные приведения неприменимы к публичным свойствам или свойствам, которые имеют переопределённые getter'ы;
  • с локальными var переменными - если переменная не изменяется между проверкой и использованием и не захватывается лямбдой, которая её модифицирует;
  • с var свойствами - никогда (потому что переменная может быть изменена в любое время другим кодом).

Оператор "небезопасного" приведения

Обычно оператор приведения выбрасывает исключение, если приведение невозможно, поэтому мы называем его небезопасным. Небезопасное приведение в Kotlin выполняется с помощью инфиксного оператора as (см. приоритеты операторов):

val x: String = y as String

Заметьте, что null не может быть приведен к String, так как String не является nullable, т.е. если y - null, код выше выбросит исключение. Чтобы соответствовать семантике приведений в Java, нам нужно указать nullable тип в правой части приведения:

val x: String? = y as String?

Оператор "безопасного" (nullable) приведения

Чтобы избежать исключения, вы можете использовать оператор безопасного приведения as?, который возвращает null в случае неудачи:

val x: String? = y as? String

Заметьте, что несмотря на то, что справа от as? стоит non-null тип String, результат приведения является nullable.

Стирание и проверка типов у Обобщений (Generics)

Котлин обеспечивает типобезопасность операций, связанных с обобщениями на этапе компиляции(compile time), в то время как информация о типе аргумента обобщения недоступна во время выполнения программы. Например для List<Foo> происходит стирание типа, что превращает его в List<*>. В связи с чем, нет способа проверить, принадлежит ли объект конкретному типу во время выполнения программы.

Учитывая это, компилятор запрещает is-проверки, которые не могут быть выполнены во время выполнения программы из-за стирания типов, например ints is List<Int> или list is T (параметризированный тип). Однако у вас есть возможность произвести проверку со "Звёздными" проекциями:

```kotlin if (something is List<*>) { something.forEach { println(it) } // Элементы типа `Any?` } ```

Таким же образом, когда у вас есть статически определенный тип аргумента, вы можете произвести is-проверку или приведение с не-обобщенной частью типа. Заметье, что в данном случае угловые скобки пропущены:

```kotlin fun handleStrings(list: List) { if (list is ArrayList) { // `list` приводится к `ArrayList` засчет "умного приведения" } } ```

Аналогичный синтаксис, с пропущенным типом аргумента может использоваться для приведений, которые не принимают типы аргументы: list as ArrayList

Встроенные (inline) функции с параметрами вещественного типа имеют свои аргументы типа, встроенные на каждый момент вызова, что позволяет arg is T проверку параметризованного типа, но если arg является объектом обобщенного типа, его аргумент типа по-прежнему стирается. Пример:

```kotlin inline fun Pair<*, *>.asPairOf(): Pair? { if (first !is A || second !is B) return null return first as A to second as B } val somePair: Pair = "items" to listOf(1, 2, 3) val stringToSomething = somePair.asPairOf() val stringToInt = somePair.asPairOf() val stringToList = somePair.asPairOf>() val stringToStringList = somePair.asPairOf>() // Нарушает типобезопасность! ```

Непроверяемые (Unchecked) приведения

Как упоминалось выше, стирание типов делает невозможным проверку типа аргумента обобщения на этапе выполнения, и обобщенные типы в коде могут быть недостаточно связаны друг с другом, чтобы компилятор обеспечил типобезопасность.

Тем не менее, иногда мы имеем программную логику высокого уровня, которая вместо этого подразумевает типобезопасность. Например:

```kotlin fun readDictionary(file: File): Map = file.inputStream().use { TODO("Прочитать сопоставление строк с произвольными элементами.") } // Мы сохранили словарь(map) `Int`ов в файл val intsFile = File("ints.dictionary") // Warning: Unchecked cast: `Map` to `Map` val intsDictionary: Map = readDictionary(intsFile) as Map ```

Компилятор выдает предупреждение для приведения в последней строке. Приведение не может быть полностью проверено во время выполнения и нет дает гарантии, что значения в словаре (map) являются Int.

Чтобы избежать непроверяемые приведения, вы можете изменить структуру программы: в примере выше, возможно объявить интерфейсы DictionaryReader<T> и DictionaryWriter<T> с типобезопасными имплементациями для типизированного типа. Правильно использование вариативности обобщений также может помочь.

Для обобщенных функций, используемых встроенные (inline) функции с параметрами вещественного типа приведение типа arg as T является проверяемым, до тех пор, если тип arg не имеет свои аргументы типа, которые были стерты.

Предупреждение о непроверяемом приведении можно убрать используя аннотации

inline fun <reified T> List<*>.asListOfType(): List<T>? =
    if (all { it is T })
        @Suppress("UNCHECKED_CAST")
        this as List<T> else
        null

В JVM, массивы сохраняют информацию о стираемом типе их элементов, а также приведение типов к массиву частично проверяется: nullability и фактические аргументы для параметризированных элементов массива все еще стираются. Например, приведение foo as Array <List <String>?> будет успешным, если foo является массивом, содержащим какой-либо List <*> , null или нет.