Что такое инвариантность Generic в Kotlin

Ответ

В Kotlin дженерики по умолчанию инвариантны (out и in не указаны). Это значит, что Box<Cat> не является ни подтипом, ни надтипом Box<Animal>, даже если Cat наследует Animal.

Пример инвариантного дженерика:

class Box<T>(var item: T)

val catBox: Box<Cat> = Box(Cat())
val animalBox: Box<Animal> = catBox // Ошибка компиляции!

Для ковариантности (Box<Cat>Box<Animal>) используется out:

class Box<out T>(val item: T)

Для контравариантности (Box<Animal>Box<Cat>) — in:

class Box<in T>(fun consume(item: T))

Ответ 18+ 🔞

Э, слушай, а вот смотри, как тут у нас в Kotlin с дженериками приколы-то. Ты ж думаешь, раз Cat — это Animal, то и коробка с котиком автоматически станет коробкой с животным? Хуй там! По умолчанию-то они, блядь, инвариантные, как будто на дворе 2002-й год и все боятся дженериков как огня. Это значит, что Box<Cat> — это вообще отдельная, ёпта, вселенная, и Box<Animal> к ней нихуя не относится. Ни папа, ни сын, просто соседи через забор.

Вот смотри, простой пример, инвариантный класс:

class Box<T>(var item: T)

val catBox: Box<Cat> = Box(Cat())
val animalBox: Box<Animal> = catBox // Ошибка компиляции! Ёперный театр!

Компилятор тебе сразу: «Чувак, ты чё, охуел?» Он тебе не даст это сделать, потому что Box<T> — это как сейф с одним типом. Если ты попробуешь подсунуть сейф для кошек под сейф для животных, он тебе скажет: «А вдруг ты потом в этот «животный» сейф засунешь Dog, а он на самом деле для Cat? Тогда будет тебе, блядь, хиросима и нигерсраки!» И он прав, сука.

Но если тебе нужно, чтобы работало, то есть два выхода, ядрёна вошь!

Первый — ковариантность (out). Это когда ты говоришь: «Э, компилятор, расслабься! Я из этой коробки только вынимать буду, ничего туда не засуну!» Ставишь out — и вуаля, Box<Cat> становится подтипом Box<Animal>.

class Box<out T>(val item: T) // out — только на выход, как из магазина

Второй — контравариантность (in). Это вообще, блядь, мозголомная штука. Ты говоришь: «Эта штука не производит T, она его потребляет!» И тогда всё идёт наоборот: Box<Animal> становится подтипом Box<Cat>. Звучит как пиздец, но это работает, когда тебе нужно что-то засунуть внутрь, а не достать.

class Box<in T> {
    fun consume(item: T) { /* ... */ } // in — только на вход, как в мясорубку
}

Короче, суть в том, что инвариантность по умолчанию — это такая хитрая жопа для твоей же безопасности, чтобы ты случайно не впендюрил собаку в коробку для кошек. А out и in — это твои пропуски, чтобы сказать системе: «Я знаю, что делаю, доверия ебать ноль, но вот мои гарантии». Вот и вся магия, не так страшен чёрт, как его малюют.