Kotlin is a powerful and flexible programming language that introduces many advanced features to make development easier. One such feature is Use-Site Variance in Kotlin, which helps handle type variance effectively in generic programming. If you’ve ever struggled with understanding variance in Kotlin, this guide will break it down in a simple, easy-to-understand way.
What is Use-Site Variance in Kotlin?
In Kotlin, variance defines how different types relate to each other in a type hierarchy when dealing with generics. Generics allow you to create flexible and reusable code, but without proper variance handling, type safety issues may arise.
Kotlin provides two types of variance:
- Declaration-Site Variance — Defined at the class level using
out
(covariance) andin
(contravariance). - Use-Site Variance — Applied at the point where a generic class is used, rather than where it is declared.
Use-Site Variance is particularly useful when you don’t have control over the generic class declaration but still need to enforce type variance.
When to Use Use-Site Variance in Kotlin?
Use-Site Variance in Kotlin is useful in scenarios where:
- You are working with a generic class that doesn’t specify variance at the declaration level.
- You need to ensure type safety when passing a generic object to a function.
- You want to restrict read and write operations based on the expected type.
Understanding when to use it is important because incorrect usage may lead to type mismatches and compilation errors.
BTW, How does use-site variance work in Kotlin?
Kotlin supports use-site variance, you can specify variance at the use site, which means you can indicate the variance for a specific occurrence of a type parameter, even if it can’t be declared as covariant or contravariant in the class declaration. Let’s break down the concepts and see how use-site works.
In Kotlin, many interfaces, like MutableList
, are not covariant or contravariant by default because they can both produce and consume values of the types specified by their type parameters. However, in certain situations, a variable of that type may be used only as a producer or only as a consumer.
Consider the function copyData
that copies elements from one collection to another:
fun <T> copyData(source: MutableList<T>, destination: MutableList<T>) {
for (item in source) {
destination.add(item)
}
}
In this function, both the source
and destination
collections have an invariant type. However, the source
collection is only used for reading, and the destination
collection is only used for writing. In this case, the element types of the collections don’t need to match exactly.
To make this function work with lists of different types, you can introduce a second generic parameter:
fun <T : R, R> copyData(source: MutableList<T>, destination: MutableList<R>) {
for (item in source) {
destination.add(item)
}
}
In this modified version, you declare two generic parameters representing the element types in the source and destination lists. The source element type (T
) should be a subtype of the elements in the destination list (R
).
However, Kotlin provides a more elegant way to express this using use-site variance. If the implementation of a function only calls methods that have the type parameter in the “out” position (as a producer) or only in the “in” position (as a consumer), you can add variance modifiers to the particular usages of the type parameter in the function definition.
For example, you can modify the copyData
function as follows:
fun <T> copyData(source: MutableList<out T>, destination: MutableList<T>) {
for (item in source) {
destination.add(item)
}
}
In this version, you specify the out
modifier for the source
parameter, which means it’s a projected (restricted) MutableList
. You can only call methods that return the generic type parameter (T
) or use it in the “out” position. The compiler prohibits calling methods where the type parameter is used as an argument (“in” position).
Here’s an example usage:
val ints = mutableListOf(1, 2, 3)
val anyItems = mutableListOf<Any>()
copyData(ints, anyItems)
println(anyItems) // [1, 2, 3]
When using use-site variance in Kotlin, there are limitations on the methods that can be called on a projected type. If you are using a projected type, you may not be able to call certain methods that require the type parameter to be used as an argument (“in” position) :
val list: MutableList<out Number> = ...
list.add(42) // Error: Out-projected type 'MutableList<out Number>' prohibits the use of 'fun add(element: E): Boolean'
Here, list
is declared as a MutableList<out Number>
, which is an out-projected type. The out
projection restricts the type parameter Number
to only be used in the “out” position, meaning it can only be used as a return type or read from. You cannot call the add
method because it requires the type parameter to be used as an argument (“in” position).
If you need to call methods that are prohibited by the projection, you should use a regular type instead of a projection. In this case, you can use
MutableList<Number>
instead ofMutableList<out Number>
. By using the regular type, you can access all the methods available for that type.
Regarding the concept of using the in
modifier, it indicates that in a particular location, the corresponding value acts as a consumer, and the type parameter can be substituted with any of its supertypes. This is similar to the contravariant position in Java’s bounded wildcards.
For example, the copyData
function can be rewritten using an in-projection:
fun <T> copyData(source: MutableList<T>, destination: MutableList<in T>) {
for (item in source) {
destination.add(item)
}
}
In this version, the destination
parameter is projected with the in
modifier, indicating that it can consume elements of type T
or any of its supertypes. This allows you to copy elements from the source
list to a destination list with a broader type.
It’s important to note that use-site variance declarations in Kotlin correspond directly to Java’s bounded wildcards.
MutableList<out T>
in Kotlin is equivalent toMutableList<? extends T>
in Java, while the in-projectedMutableList<in T>
corresponds to Java’sMutableList<? super T>
.
Use-site projections in Kotlin can help widen the range of acceptable types and provide more flexibility when working with generic types, without the need for separate covariant or contravariant interfaces.
Conclusion
Use-Site Variance in Kotlin is a powerful feature that ensures type safety while working with generics. By using out
when you only need to retrieve values and in
when you only need to pass values, you can effectively manage type variance without modifying class declarations.
Understanding how and when to use Use-Site Variance in Kotlin can help you write cleaner, more flexible, and safer generic code. Start experimenting with it in your Kotlin projects and see how it simplifies handling type variance..!