What does in/out actually do in Kotlin when passed as arguments?
Solution 1:
Let me demonstrate what in
/out
do with the help of an example. Consider the following:
private fun foo(list: ArrayList<Number>) {}
private fun bar(list: ArrayList<Number>) {}
Now we try to pass an ArrayList
to each function, each with a different generic type parameter:
// Error: Type Mismatch. Required `ArrayList<Number>` Found `ArrayList<Int>`
foo(arrayListOf<Int>())
// Error: Type Mismatch. Required `ArrayList<Number>` Found `ArrayList<Any>`
bar(arrayListOf<Any>())
But we get errors! How do we solve that? We have to tell the compiler somehow that, for foo
the list can also contain elements of a subtype of Number
(e.g. Int
) and for bar
we have to tell the compiler that the list can also contain elements of a basetype of Number
(e.g. Any
).
private fun foo(list: ArrayList<out Number>) {}
private fun bar(list: ArrayList<in Number>) {}
And now it works!
Further reading
Solution 2:
Type Projection
When the modifier in
or out
is used in the function parameter, it's called type projection.
Projections | Produces | Consumes | Function Behaviour |
---|---|---|---|
ArrayList<out Orange> |
Orange |
Nothing |
Accepts subtypes of ArrayList<Orange>
|
ArrayList<in Orange> |
Any? |
Orange |
Accepts supertypes of ArrayList<Orange>
|
ArrayList<Orange> |
Orange |
Orange |
Doesn't accept any subtypes or supertypes |
ArrayList
in Kotlin is a producer as well as a consumer. This is because it's an invariant generic class defined as ArrayList<T>
,not as ArrayList<out T>
(producer) or ArrayList<in T>
(consumer). This means the class can have functions that accept T
as a function parameter (consume) or return T
(produce).
But what if you want to use this already existing class safely just as a consumer(in T
) or just as a producer(out T
)? Without worrying about the accidental use of the other unwanted functionality?
In that case, we project the types by using variance modifiers in
and out
at the use-site. Use-site simply means wherever we use the ArrayList<T>
class.
out
produces T
and the function accepts subtypes
When you are using the ArrayList
as a producer(out
), the function can accept the subtypes of ArrayList<Orange>
, that is, ArrayList<MandarinOrange>
, ArrayList<BloodOrange>
, since MandarinOrange
and BloodOrange
are subtypes of Orange
. Because the subtyping is preserved:
fun useAsProducer(producer: ArrayList<out Orange>) {
// Producer produces Orange and its subtypes
val orange = producer.get(1) // OK
// Can use functions and properties of Orange
orange.getVitaminC() // OK
// Consumer functions not allowed
producer.add(BloodOrange()) // Error
}
The producer
produces Orange
and its subtypes. Here producer.get(1)
can return MandarinOrange
, BloodOrange
etc. but we don't care as long as we get an Orange
. Because we are only interested in using the properties and functions of Orange
at use-site.
Compiler doesn't allow calling the add()
function (consumer) because we don't know which type of Orange
it contains. You don't want to accidentally add BloodOrange
, if this is an ArrayList<MandarinOrange>
.
in
consumes T
and the function accepts supertypes
When you are using ArrayList
as a consumer(in
), the function can accept the supertypes of ArrayList<Orange>
, that is, ArrayList<Fruit>
because now the subtyping is reversed. This means ArrayList<Fruit>
is a subtype of ArrayList<Orange>
when Orange
is a subtype of Fruit
:
fun useAsConsumer(consumer: ArrayList<in Orange>) {
// Produces Any?, no guarantee of Orange because this could
// be an ArrayList<Fruit> with apples in it
val anyNullable = consumer.get(1) // Not useful
// Not safe to call functions of Orange on the produced items.
anyNullable.getVitaminC() // Error
// Consumer consumes Orange and its subtypes
consumer.add(MandarinOrange()) // OK
}
The consumer
consumes Orange
and its subtypes. It doesn't matter whether it's a MandarinOrange
or a BloodOrange
as long as it's an Orange
. Because the consumer
is only interested in the properties and functions of Orange
at its declaration-site.
The compiler does allow the call to get()
function (producer) but it produces Any?
which is not useful for us. The compiler flags an error when you use that Any?
as an Orange
at use-site.
Invariant produces and consumes T
, the function doesn't accept subtypes or supertypes
When you are using ArrayList
as a producer and consumer (without in
or out
), the function can accept only the exact type ArrayList<Orange>
, no other subtypes like ArrayList<MandarinOrange>
or supertypes like ArrayList<Fruit>
. Because the subtyping is not allowed for invariants:
fun useAsProducerConsumer(producerConsumer: ArrayList<Orange>) {
// Produces Orange and its subtypes
val orange = producerConsumer.get(1) // OK
// Orange is guaranteed
orange.getVitaminC() // OK
// Consumes Orange and its subtypes
producerConsumer.add(Orange()) // OK
}
The invariant produces and consumes Orange
and its subtypes.
That's it! Type projection is all about telling the compiler how you are using that class in that particular function, so it can help you by flagging an error if you call unintended functions accidentally. Hope that helps.
Solution 3:
For in generic, we could assign a class of super-type to class of sub-type. But for out generic, we could assign a class of sub-type to class of super-type