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