Declaration-Site Variance is variance that is specified at the point where the generic is defined – for example, in the definition of the class, interface, function, or extension property.
To use declaration-site variance, you just have to modify a type parameter with either the in
or out
variance annotation. By doing this, you limit where you’re allowed to use the type parameter, but you also introduce subtyping for your generic, so that, for example, a Box<Dog>
can be a subtype of Box<Animal>
, whereas otherwise this wouldn’t be possible.
Declaration-site variance is kind of like a generic that takes a vow. For each type parameter, there are two kinds of vows that it can take:
- It can vow to never accept an object of that type parameter. In other words, it’s a vow that the type parameter will never appear as a parameter of a non-private function.
- It can vow to never return an object of that type parameter. In other words, it’s a vow that the type parameter will never appear as the return type of a non-private function.
A type can only make one of these two vows for each type parameter, but it’s allowed to make different vows for different type parameters.
Examples
To start with, let’s look at a class that does not include any variance at all:
class Box<T>(private var item: T) {
fun getItem(): T = item
fun setItem(newItem: T) {
item = newItem
}
}
(Yes, normally, you’d just use Kotlin’s support for properties, by simply writing class Box<T>(var item: T)
, but to help demonstrate the concepts clearly, we’re going to use our own hand-written getter and setter.)
In this example, the class Box
is what we call “invariant”, meaning that Box<Dog>
is not a subtype of Box<Animal>
, nor the other way around.
Vow #1: out
To take the first kind of vow, we simply add the out
modifier to the type parameter declaration:
class ReadOnlyBox<out T>(private var item: T) {
fun getItem(): T = item
}
You might recall that in our first example above, the type parameter T
showed up as a function parameter in setItem()
. But because ReadOnlyBox
has promised to never accept a T
in a function, we had to remove the setItem()
function entirely. Otherwise, we’ll get a compiler error:
Type parameter T is declared as ‘out’ but occurs in ‘in’ position in type T
So… we’ve limited what we can do with the class. This can be a good thing, though, because the trade-off is covariance. Because we added out
, ReadOnlyBox<Dog>
will be regarded as a subtype of ReadOnlyBox<Animal>
.
Wait, what about that constructor? The type parameter T
shows up as a parameter there!
Yep, that’s fine! The constructor doesn’t count!
It’s also fine to accept the type parameter as a function parameter if the function is marked private
. For example, our ReadOnlyBox
class could include the setItem()
function as long as it’s marked private
, because we know that the value of newItem
will never come from outside the class.
Vow #2: in
To take the second kind of vow, simply add the in
modifier to the type parameter:
class WriteOnlyBox<in T>(private var item: T) {
fun setItem(newItem: T) {
item = newItem
}
}
In this case, we’ve got the opposite arrangement from the ReadOnlyBox
- the type parameter is not allowed as the return type from a function, so we had to remove the getItem()
function. The setItem()
function, on the other hand, is just fine.
If we were to try to include the getItem()
function in the class without marking it private
, we’d get a compiler error that’s the counterpart to the one above:
Type parameter T is declared as ‘in’ but occurs in ‘out’ position in type T
The benefit to marking the type parameter with in
is that it creates contravariance, which means that, for example, a WriteOnlyBox<Animal>
will be a subtype of WriteOnlyBox<Dog>
. (You read that right! In this case, a WriteOnlyBox
of the more general type Animal
is actually a subtype of a WriteOnlyBox
of the more specific type Dog
!)
Just like in the previous case, our class can do whatever it wants with the type parameter as long as it’s not exposed. So, we can put getItem()
back - but we’d have to make it private
.
Declaration-Site Variance and Properties
The examples above demonstrated effects of declaration-site variance on the type parameter in either function parameter types or function return types. But it’s important to note that properties are also affected by these vows. For example:
class Box<out T>(val item: T)
This class has its type parameter marked as out
, which works fine because val
means that the property is read-only. But the following would not work:
class Box<out T>(var item: T) // doesn't compile!
That’s because in this example, item
is marked as a var
, which means it’s read and write. In other words, it has an implicit setter, which puts the type parameter T
in an “in” position, which we promised we wouldn’t do.
Declaration-Site Variance and Inheritance
So far we’ve looked at variance from the standpoint of isolated classes, like Box
, ReadOnlyBox
, and WriteOnlyBox
. But how does it work when inheritance is involved?
First, it’s important to note that type parameters aren’t actually inherited. They’re redefined in the subclass, where they can be used when declaring the supertype:
In this case, we’re passing along MutableBox
’s type parameter X
as a type argument to Box
. To help clarify that, you can see I used a different name for the type parameter in each of those classes. MutableBox
cannot reference T
- it can only reference its own type parameter, X
.
So, how does the variance of a supertype affect the subtype? Here’s the rule:
For each type parameter, a subtype can declare the same variance as its supertype, or it can effectively remove the variance declared on its supertype.
In other words, you cannot introduce variance, and you cannot change the variance (e.g., from out
to in
).
This makes sense. If the supertype made no promises as to how it would use the type parameter (that is, it doesn’t mark its type parameter as either in
or out
), then adding in
or out
to the subtype’s type parameter could possibly be inconsistent. The subtype can’t go making promises that the supertype didn’t make.
Breaking Your Vows
Once a generic has taken a vow, is it possible for it to break that vow? Yes, you can cheat on those promises! But be careful about it. You’ll lose the benefit of compile-time type guarantees, and you could end up with a ClassCastException
at runtime.
To break your vow, annotate a type parameter reference with @UnsafeVariance
. For example:
class UnsafeBox<out T>(private var item: T) {
fun get(): T = item
fun set(newItem: @UnsafeVariance T) {
item = newItem
}
}
Even though UnsafeBox
declares its type parameter with out
, it still accepts T
as a function parameter! Here’s an example of how things can go wrong:
open class Animal
open class Dog : Animal()
open class Schnauzer : Dog()
// Because T is "out", when you call setIt(), you can pass
// either UnsafeBox<Dog> or UnsafeBox<Schnauzer>:
fun setIt(box: UnsafeBox<Dog>, dog: Dog) = box.set(dog)
val box: UnsafeBox<Schnauzer> = UnsafeBox(Schnauzer())
setIt(box, Dog()) // Oops! Passing a Dog to UnsafeBox<Schnauzer>
val schnauzer: Schnauzer = box.get() // ClassCastException!
This is especially nefarious because the exception doesn’t happen when the object is incorrectly saved to the Box
. It happens when you go to pull the item back out of it. Feels kinda like a NullPointerException
, doesn’t it?
Well, why would @UnsafeVariance
ever be helpful then?
Let’s rework our UnsafeBox
. We’ll scrap the set()
function, and introduce a new function called has()
, which tells us if the object we pass in is the same as the one in the box.
class Box<out T>(private var item: T) {
fun get(): T = item
fun has(other: @UnsafeVariance T) = item == other
}
In this case, the other
parameter in the has()
function is never saved as object state - it’s only used for a comparison. If we were to end up comparing objects of different types, nothing would break. So we know that we won’t end up with a ClassCastException
here.
Indeed, this is how the Kotlin standard library uses the @UnsafeVariance
annotation - for testing a value, or finding the index of an object in a collection.
Alternatives and Trade-offs
The primary alternative to declaration-site variance is use-site variance, where the code using the generic agrees to treat it as if it had taken one of these vows.
In general, declaration-site variance is preferred for any code that’s under your control because the variance is specified only once, and all use sites get the benefit of the variance.
On the other hand, if you know that your generic will be used in different ways at different use sites, then using declaration-site variance would likely also mean creating different interfaces to separate the read and write functions. This is the main disadvantage. For example:
interface Box<out T> {
fun get(): T
}
interface MutableBox<T> : Box<T> {
fun set(item: T)
}
But in general, favor declaration-site variance for code that you control, and take advantage of use-site variance for code that you don’t control, such as classes from libraries.