Почему класс / интерфейс не может иметь префикс out, если класс / интерфейс имеет свойство val или функцию с универсальным типом?

#java #kotlin #generics

#java #kotlin #общие сведения

Вопрос:

Изучая generics в Kotlin, я прочитал в книге следующее :

В общем случае универсальный тип класса или интерфейса может иметь префикс out, если класс имеет функции, которые используют его в качестве возвращаемого типа, или если класс имеет свойства val этого типа. Однако вы не можете использовать out, если класс имеет параметры функции или свойства var этого универсального типа.

Я понимаю, что говорится в правиле, но я буду рад понять (на примерах), что может быть без этого правила (т.е. Не было ограничений при использовании out при объявлении универсального класса / интерфейса), а также почему не «опасно», что возвращаемый тип может быть изтип T и все же класс / интерфейс могут содержать out T.

Пример, в котором не могу понять, в чем проблема, что свойство класса будет вести себя как ковариантное:

    class Pet{....}
class Dog:Pet{...}

class PetSomething <T : Pet>  
{
    T t;
    public fun petDoSomething(T t)
    {
        ....   // what can be the problem here?
    }
}


class DogSomething
{
    dogDoSomething()
    {
        d : Dog = Dog()
        petDoSomething(d)
        //what is the problem here???
    }
}
  

Кроме того, в книге отображается следующий код:

abstract class E<out T> (t:T) { val x = t }

и код компилируется, хотя универсальный тип является вводом конструктора. Разве это не нарушает правило?

Комментарии:

1. Можете ли вы перефразировать эту первую часть своего вопроса? Я не понимаю. «что может быть без этого правила (т.Е. Не было ограничений при использовании out при объявлении универсального класса / интерфейса)»

2. @Tenfour04, я добавил пример к вопросу. Может быть, это поможет вам понять, о чем я спрашиваю. Если нет, я попробую другой способ, потому что это немного сложно для меня.

Ответ №1:

Вы процитировали: «Однако вы не можете использовать out, если класс имеет параметры функции или свойства var этого универсального типа».

Конструктор не является функцией-членом или свойством, поэтому на него не распространяется это правило. Безопасно использовать тип для параметра на сайте конструктора, потому что тип известен при его создании.

Рассмотрим эти классы:

 abstract class Pet

class Cat: Pet()
class Dog: Pet()

class PetOwner<out T: Pet>(val pet: T)
  

Когда вы вызываете конструктор PetOwner и передаете a Cat , компилятор знает, что вы создаете a PetOwner<out Cat> , потому что он знает, что значение, переданное конструктору, удовлетворяет типу <out Cat> . Перед созданием объекта не нужно Cat выполнять Pet преобразование в . Затем созданный объект может быть безопасно передан в PetOwner<Pet> , потому что no T никогда не будет передан экземпляру снова. Нет ничего небезопасного, что может произойти, потому что для параметра не выполняется приведение.

Параметры и var свойства функции были бы небезопасны для out типа, потому что объект уже создан и мог быть передан некоторой переменной, которая уже передала его чему-то другому.

Представьте, что компилятор позволяет вам определять out T для такого var свойства, как это:

 class PetOwner<out T: Pet>(var pet: T)
  

Тогда вы могли бы сделать это:

 val catOwner: PetOwner<out Cat> = PetOwner(Cat())
val petOwner: PetOwner<out Pet> = catOwner
petOwner.pet = Dog()
val cat: Cat = catOwner.pet // ClassCastException!
  

Правила безопасности типов исключают возможность такого сценария. Но это невозможно для val параметра конструктора. Нет способа передать объект другим переменным и повысить его тип между передачей параметра конструктору и созданием экземпляра, который вы можете передавать.

Комментарии:

1. «Конструктор не является функцией-членом или свойством, поэтому на него не распространяется это правило». Однако сформулированное правило на самом деле не является полным (у вас не может быть var pets: List<T> ни того, ни другого).

2. @Tenfour04, спасибо за объяснение и за пример (примеры действительно помогают в подобном случае). Не могли бы вы также сослаться на пример, который я создал в своем сообщении, и сказать мне, есть ли там ситуация с ClassCastException ?

3. Ваш пример ничем не отличается от моего выше, за исключением того, что он не использует ярлык для определения свойства в конструкторе. Свойство по-прежнему присваивается при создании экземпляра. Компилятор выдаст сообщение об ошибке, если вы сделаете что-то небезопасное с дженериками, или выдаст предупреждение, если вы выполните потенциально небезопасное приведение вручную (с использованием as ключевого слова). Мой приведенный выше пример не будет компилироваться, поскольку вы не можете комбинировать out с типом, используемым для var свойства.

Ответ №2:

Проблема заключается в следующем:

 val x = DogSomething() 
val y: PetSomething<Pet> = x // would be allowed by out
y.petDoSomething(Cat())
  

Обратите внимание, что petDoSomething on DogSomething должен обрабатывать только Dog s .

и код компилируется, хотя универсальный тип является вводом конструктора. Разве это не нарушает правило?

Это не так, потому что конструктор не является членом в соответствующем смысле; его нельзя было вызвать y выше.

Ответ №3:

Сначала давайте проясним, что мы получаем, добавляя префикс a type parameter with out keyword . рассмотрим следующее class :

 class MyList<out T: Number>{
    private val list: MutableList<T> = mutableListOf()
    operator fun get(index: Int) : T = list[index]
}
  

out ключевое слово здесь делает MyList ковариантным in T , что, по сути, означает, что вы можете сделать следующее :

 // note that type on left side of = is different than the one on right

val numberList: MyList<Number> = MyList<Int>()
  

если вы удалите ключевое слово out и попытаетесь скомпилировать снова, вы получите ошибку несоответствия типов.

добавляя префикс type parameter with out , вы в основном объявляете type , что он является производителем T ‘s , в приведенном выше примере MyList это производитель чисел. это означает, что независимо от того, являетесь ли вы instantiate T or Int Double или каким-либо другим подтипом Number , вы всегда сможете получить число из MyList (потому что каждый подтип Number является a Number ). который также позволяет вам выполнять следующие действия:

 fun process(list: MyList<Number>) { // do something with every number }
fun main(){
  val ints = MyList<Int>()
  val doubles = MyList<Double>()
  process(ints)     // Int is a Number, go ahead and process them as Numbers
  process(doubles)  // Double is also a Number, no prob here
}
// if you remove out, you can only pass MyList<Number> to process
  

Теперь давайте ответим с out ключевым словом, почему T оно должно быть только в позиции возврата?и что может произойти без этого ограничения?, то есть если MyList бы T в качестве параметра использовалась функция.

 fun add(value: T) { list.add(T) }  // MyList has this function

fun main() {
    val numbers = getMyList()   // numbers can be MyList<Int>, MyList<Double> or something else
    numbers.add(someInt)        // cant store Int, what if its MyList<Double> ( Int != Double) 
    numbers.add(someDouble)     // cant do this, what if its MyList<Int>
}

// We dont know what type of MyList we going to get
fun getMyList(): MyList<Number>(){
  return if(feelingGood) { MyList<Int> () }
         else if(feelingOk> { MyList<Double> () }
         else { MyList<SomeOtherSubType>() }
}
  

вот почему требуется ограничение, оно в основном предназначено для обеспечения безопасности типов.

что касается abstract class E<out T> (t:T) { val x = t } компиляции, Kotlin в действии может сказать следующее

Обратите внимание, что параметры конструктора не находятся ни в позиции in, ни out . Даже если параметр типа объявлен как out , вы все равно можете использовать его в параметре конструктора. Отклонение защищает экземпляр класса от неправильного использования, если вы работаете с ним как с экземпляром более общего типа: вы просто не можете вызывать потенциально опасные методы. Конструктор не является методом, который можно вызвать позже (после создания экземпляра), и поэтому он не может быть потенциально опасным.

Комментарии:

1. прошу прощения за то, что ответил на ваш ответ через две недели после него. Но до сегодняшнего дня я «сражался», чтобы понять дженерики. Итак, только сегодня у меня достаточно (я надеюсь) знаний, чтобы ответить на ваш пост. Можно ли создать / найти пример, который не работает со списком, стеком и т. Д.? Потому что все примеры в сети отображают проблему, которая может возникнуть, если мы «испортим» список. Есть ли способ найти пример, который не включает эти структуры данных ?