На этом шаге мы рассмотрим необходимость использования этих конструкций.
Для дальнейших изменений ваших параметров обобщенного типа Kotlin предоставляет ключевые слова in и out. Чтобы посмотреть на их работу, создайте простой обобщенный класс Barrel в новом файле с именем Variance.kt.
class Barrel<T>(var item: T)
Рис.1. Объявление Barrel (Variance.kt)
Для экспериментов с Barrel добавим функцию main(). В main() объявите Barrel для хранения Fedora и еще один Barrel для хранения Loot.
class Barrel<T>(var item: T) fun main(args: Array<String>) { var fedoraBarrel: Barrel<Fedora> = Barrel(Fedora("a generic-looking fedora", 15)) var lootBarrel: Barrel<Loot> = Barrel(Coin(15)) }
Рис.2. Объявление двух Barrel в main() (Variance.kt)
Хотя Barrel<Loot> может хранить награды любого типа, конкретный объявленный экземпляр в примере хранит Coin (подкласс Loot).
Теперь присвойте переменной lootBarrel экземпляр fedoraBarrel.
class Barrel<T>(var item: T) fun main(args: Array<String>) { var fedoraBarrel: Barrel<Fedora> = Barrel(Fedora("a generic-looking fedora", 15)) var lootBarrel: Barrel<Loot> = Barrel(Coin(15)) lootBarrel = fedoraBarrel }
Рис.3. Попытка присвоить значение lootBarrel (Variance.kt)
Удивительно, но эта попытка будет отвергнута компилятором (рисунок 3, несоответствие типа).
Казалось бы, такое присваивание должно быть допустимо. Fedora все-таки является наследником Loot, и переменной типа Loot вполне можно присвоить экземпляр Fedora:
var loot: Loot = Fedora("a generic-looking fedora", 15) // Нет ошибки
Чтобы понять, в чем проблема, давайте поразмышляем, что получится, если такое присваивание выполнится успешно.
Если компилятор разрешит присвоить экземпляр fedoraBarrel переменной lootBarrel, тогда lootBarrel будет указывать на fedoraBarrel и появится возможность взаимодействовать с наградой в fedoraBarrel так, будто это Loot, а не Fedora (потому что тип lootBarrel - это Barrel<Loot>).
Например, монеты - это один из подтипов Loot, поэтому свойству item экземпляра lootBarrel (который указывает на fedoraBarrel) можно было бы присвоить монеты. Сделайте так в Variance.kt.
class Barrel<T>(var item: T) fun main(args: Array<String>) { var fedoraBarrel: Barrel<Fedora> = Barrel(Fedora("a generic-looking fedora", 15)) var lootBarrel: Barrel<Loot> = Barrel(Coin(15)) lootBarrel = fedoraBarrel lootBarrel.item = Coin(15) }
Рис.4. Присваивание монет свойству lootBarrel.item (Variance.kt)
Теперь предположим, что вы обращаетесь к fedoraBarrel.item, ожидая получить шляпу.
class Barrel<T>(var item: T) fun main(args: Array<String>) { var fedoraBarrel: Barrel<Fedora> = Barrel(Fedora("a generic-looking fedora", 15)) var lootBarrel: Barrel<Loot> = Barrel(Coin(15)) lootBarrel = fedoraBarrel lootBarrel.item = Coin(15) val myFedora: Fedora = lootBarrel.item }
Рис.5. Обращение к fedoraBarrel.item (Variance.kt)
Компилятор столкнется с несоотвествием типа - fedoraBarrel.item не Fedora, а Coin, - и вы получите исключение ClassCastException. Теперь у вас возникла проблема, и именно по этой причине компилятор изначально запретил присваивание.
Для решения этой проблемы и были придуманы ключевые слова in и out.
В объявлении класса Barrel добавьте ключевое слово out и измените item c var на val.
class Barrel<out T>(val item: T)
Рис.6. Добавление out (Variance.kt)
Далее удалите строку кода, в которой присваивали Coin свойству item (что теперь запрещено, так как item уже val), и присвойте свойству lootBarrel.item переменную myFedora вместо fedoraBarrel.item.
class Barrel<out T>(val item: T) fun main(args: Array<String>) { var fedoraBarrel: Barrel<Fedora> = Barrel(Fedora("a generic-looking fedora", 15)) var lootBarrel: Barrel<Loot> = Barrel(Coin(15)) lootBarrel = fedoraBarrel val myFedora: Fedora = lootBarrel.item }
Рис.7. Изменение присваивания (Variance.kt)
На следующем шаге мы закончим изучение этого вопроса.