2
votes

When learning generics in Kotlin, I read in a book the following :

In general, a class or interface generic type may be prefixed with out if the class has functions that use it as a return type, or if the class has val properties of that type. You can’t, however, use out if the class has function parameters or var properties of that generic type.

I understand what the rule says, but i will be happy to understand (by examples) what may be without this rule (i.e there weren't constraint when using out when declaring a generic Class/Interface), and also why it isn't "dangerous" that the return type can be from type T and still class/Interface can contain out T.

Example where can't understand what is the problem that class property will behave as covariant:

   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???
    }
}

In addition the book display the following code:

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

and the code is being compiled although generic type is an input of constructor. Doesn't it break the rule?

3
Can you rephrase this first part of your question? I don't understand. "what may be without this rule (i.e there weren't constraint when using out when declaring a generic Class/Interface)"Tenfour04
@Tenfour04, i have added an example to the question. Maybe it help you to understand what i'm asking. If not i will try another way cause it's a bit difficult for me.Eitanos30

3 Answers

1
votes

You quoted: "You can’t, however, use out if the class has function parameters or var properties of that generic type."

A constructor is not a member function or property, so it is not subject to this rule. It is safe to use the type for a parameter at the site of the constructor, because the type is known when you are constructing it.

Consider these classes:

abstract class Pet

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

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

When you call the PetOwner constructor and pass in a Cat, the compiler knows you are constructing a PetOwner<out Cat> because it knows the value passed to the constructor satisfies the type of <out Cat>. It doesn't have to upcast Cat to Pet before the object is constructed. Then the constructed object can be safely upcast to PetOwner<Pet> because no T is ever going to be passed to the instance again. There is nothing unsafe that can happen, because no casting is done to the parameter.

Function parameters and var properties would be unsafe for an out type because the object is already constructed and might have been passed to some variable that has already upcast it to something else.

Imagine that the compiler let you define out T for a var property like this:

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

Then you could do this:

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

The type safety rules prevent this scenario from being possible. But this isn't possible for a val constructor parameter. There is no way to pass the object to other variables and upcast its type in between passing the parameter to the constructor and having an instance that you can pass around.

0
votes

The problem is this:

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

Note that petDoSomething on DogSomething only has to handle Dogs.

and the code is being compiled although generic type is an input of constructor. Doesn't it break the rule?

It doesn't, because the constructor isn't a member in the relevant sense; it couldn't be called on y above.

0
votes

First lets clearfy what do we get by prefixing a type parameter with out keyword. consider the following class:

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

out keyword here makes MyList covariant in T, which essentially means you can do the following :

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

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

if you remove the out keyword and try to compile again, you will get type mismatch error.

by prefixing the type parameter with out, you are basically declaring the type to be a producer of T's, in the example above MyList is a producer of Numbers. which means no matter if you instantiate T as Int or Double or some other subtype of Number, you will always be able to get a Number from MyList (Because every subtype of Number is a Number). which also allows you to to do the following:

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

Now lets answer With out keyword why T should only be in return position? and what can happen without this constraint?, that is if MyList had a function taking T as parameter.

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>() }
}

that is why constraint is required, its basically there to guarantee type safety.

as for abstract class E<out T> (t:T) { val x = t } being compiled, Kotlin In Action has following to say

Note that constructor parameters are in neither the in nor the out position. Even if a type parameter is declared as out, you can still use it in a constructor parameter. The variance protects the class instance from misuse if you’re working with it as an instance of a more generic type: you just can’t call the potentially dangerous methods. The constructor isn’t a method that can be called later (after an instance creation), and therefore it can’t be potentially dangerous.