Шаг 206.
Основы Kotlin.
Обобщения. Конструкции in и out (окончание)

    На этом шаге мы рассмотрим назначение и примеры использования этих конструкций.

    Все ошибки исправлены. Что изменилось?

    Параметр обобщенного типа может играть две роли: потребитель и производитель. Роль производителя подразумевает, что обобщенный параметр будет доступен для чтения (но не для записи), а роль потребителя означает, что обобщенный параметр будет доступен для записи (но не для чтения).

    Добавив ключевое слово out в Barrel<out T>, вы указали, что обобщенный параметр будет вести себя как производитель, то есть будет доступен для чтения, но не для записи. Это значит, что объявление item с ключевым словом var более не разрешено. В противном случае оно не будет производителем для Fedora, а будет доступно для записи и станет потребителем.

    Сделав обобщение производителем, вы убедили компилятор в том, что проблема, описанная ранее, больше не появится: так как обобщенный параметр теперь производитель, а не потребитель, переменная item не будет изменяться. Kotlin разрешит присвоить экземпляр fedoraBarrel переменной lootBarrrel, потому что это безопасно: свойство item экземпляра lootBarrel теперь имеет тип Fedora, а не Loot и не сможет измениться.

    Теперь обратите внимание на присваивание переменной myFedora в IntelliJ. Зеленая заливка вокруг lootBarrel означает, что было применено умное приведение типа, в чем легко убедиться, если навести на него указатель мыши (рисунок 1).


Рис.1. Умное приведение типа Barrel<Fedora>

    Компилятор приводит тип Barrel<Loot> к Barrel<Fedora>, так как свойство item нельзя изменить, ведь оно является производителем.

    Кстати, списки тоже являются производителями. В Kotlin в объявлении списка параметр обобщенного типа отмечен ключевым словом out:

public interface List<out E> : Collection<E>

    Объявление параметра обобщенного типа в Barrel ключевым словом in даст противоположный эффект при попытке присвоить экземпляр Barrel: теперь вы не сможете присвоить экземпляр fedoraBarrel переменной lootBarrel, но сможете присвоить экземпляр lootBarrel переменной fedoraBarrel.

    Обновите код Barrel, поменяв ключевое слово out на in. Вы заметите, что Barrel теперь требует убрать ключевое слово val из определения item, потому что иначе получится производитель (в нарушение роли потребителя).


Рис.2. Замена out на in в объявлении Barrel (Variance.kt)

    Теперь

lootBarrel = fedoraBarrel
в main() вызовет ошибку несоответствия типов (рисунок 2). Поменяйте стороны присваивания местами.
class Barrel<in T>(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))

    fedoraBarrel = lootBarrel
}
Файл с проектом можно взять здесь.


Рис.3. Перемена сторон присваивания местами (Variance.kt)

    Обратное присваивание стало возможным благодаря тому, что теперь компилятор уверен, что вы уже никогда не сможете получить награду Loot из Barrel, в котором хранится Fedora, что приводит к исключению ClassCastException. Вы убрали ключевое слово val из Barrel, потому что Barrel теперь потребитель, то есть принимает значение, но не производит его. Также вы убрали инструкцию извлечения награды. Теперь компилятор может сделать вывод, что такое присваивание безопасно.

    Кстати, может быть, вы слышали о терминах ковариантность (covariance) и контравариантность (contravariance), которые описывают то, что делают out и in. По нашему мнению, эти термины очень туманно описывают работу in и out, поэтому мы постараемся избегать их. Мы упомянули их потому, что вы можете столкнуться с ними где-нибудь еще, но теперь будете знать: если слышите "ковариантность", то представляйте out, а если "контравариантность", то представляйте in.

    В этих шагах вы узнали, как использовать обобщения для расширения возможностей классов в языке Kotlin. Увидели, как можно ограничивать обобщенные типы и использовать ключевые слова in и out для определения роли производителя и потребителя обобщенного параметра.

    В следующих шагах мы изучим расширения, позволяющие совместно использовать функции и свойства без наследования. Мы используем их для улучшения кода NyetHack.

    На следующем шаге мы рассмотрим ключевое слово reified.




Предыдущий шаг Содержание Следующий шаг