Delegated property interfaces are not very useful for illustrating variance, because they are optional and you never actually directly call these functions yourself. And you're unlikely to build a class hierarchy with them or assign instances of them to variables.
The variance is not useful in this case, but rather simply suggests the compatibility.
In the case of delegates, thisRef
has a contravariant type, indicating that a read-only delegate that is scoped to work within a certain class, is also allowed to be used within subclasses of that class.
Suppose you have a Cat class that meows and a delegate implementation that makes it meow whenever you read a delegated property:
open class Cat {
fun meow() {
println("meow")
}
}
class MeowingDelegate<T>(private val value: T): ReadOnlyProperty<Cat, T> {
override fun getValue(thisRef: Cat, property: KProperty<*>): T {
thisRef.meow()
return value
}
}
You can use this delegate within a subclass of Cat
because a subclass will have the methods of the super class, so the subclass also is also capable of meowing. The signature suggests that a MeowingDelegate with its in Cat
type qualifies as a subclass of a delegate with a more specific type and can therefore be used in a Cat subclass.
open class Kitten: Cat() {
val x: Int by MeowingDelegate(1)
}
Edit:
For the covariant T
for ReadOnlyProperty, it means that a delegate that provides one type of object can also be used as a provider of the supertype of that object. So if I had a delegate that provides Kittens, it could also be used to provide Cats:
open class Cat
open class Kitten: Cat()
class KittenDelegate: ReadOnlyProperty<Any?, Kitten> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Kitten {
return Kitten()
}
}
class Sample {
val cat: Cat by KittenDelegate()
}
Since the type T
is invariant for the ReadWriteProperty, you can't do this with a Delegate that can only return Kittens. This makes sense, because the delegate also has to allow you to set the property, and so it wouldn't be able to accept any Cat and treat it as a Kitten.
Again, these delegate interfaces are not great examples for understanding variance since they are not typically used as interfaces. For a more practical example, you can look at familiar collection classes.
A read-only List
has a covariant type. This allows you to easily cast collections to more specific types:
class Kennel (val cats: List<Cats>)
val kittens: List<Kitten> = listOf(Kitten(), Kitten())
val kennel = Kennel(kittens) // OK
You cannot do this with MutableList
, since the type is invariant.
class Kennel (val cats: MutableList<Cats>)
val kittens: MutableList<Kitten> = mutableListOf(Kitten(), Kitten())
val kennel = Kennel(kittens) // Compiler error
But it's necessary for MutableList to be defined with an invariant type. A MutableList<Kitten>
cannot accept any type of Cat to be added to it, so it wouldn't make sense to be able to cast it to a MutableList<Cat>
. Conversely, a MutableList<Cat>
does not necessarily have exclusively Kittens in it, so it doesn't make sense to be able to cast it to a MutableList<Kitten>
. Of course, at the use site, you can give it variance, depending on your needs, and this is why a MutableList<Kitten>
could be cast to a MutableList<out Cat>
.
For class-level contravariance, I can't think of a common stdlib class that has it in the declaration. So suppose we create one that pets cats. Then we have some kittens that we want to pet. A CatPetter
would be sufficient to use as a Petter<Kitten>
because Kittens qualify as Cats and can therefore be pet by a CatPetter
. If the type of Petter
were invariant, this would not be allowed.
open class Cat {
fun purr() {
println("rrrr")
}
}
open class Kitten: Cat()
interface Petter<in T: Cat> {
fun pet(recipient: T)
}
class CatPetter: Petter<Cat> {
override fun pet(recipient: Cat) {
recipient.purr()
}
}
class BoxOfKittens (val petter: Petter<Kitten>)
val x = BoxOfKittens(CatPetter()) // OK