The Programmers Guide To Kotlin - Covariance & Contravariance |
Written by Mike James | ||||
Monday, 21 January 2019 | ||||
Page 2 of 3
Covariant & Contravariant GenericsWhat else is a generic declaration than a transformation from a type T to another type G(T)? For example Array<T> converts an Int into Array<Int>. Next we come to the question of whether an array, considered as a transformation on the type of its element, is covariant, contravariant or invariant. After all, it has to be one of these three, even if you don't know what covariant, contravariant or invariant actually mean! Put in its most practical terms, given that Int is a derived class of Any: Array<Int> could be a derived class of: Array<Object> which would make Array covariant. It could be a super class of: Array<Object> which would make Array contravariant or it could have no relationship with: Array<Object> which would make Array invariant As already said, you have to answer this question, even if you don't use the academic sounding terms taken from category theory and physics. In Java and most other languages Array<Int> can be treated as if it was Array<Any> and this means it is covariant. This works well when array use fits in with the idea that outputs are covariant. For example, consider this code, which would be perfectly acceptable if arrays were treated as covariant: var a:Array<Any> Note: this doesn't actually work as written because in Kotlin arrays are invariant not covariant, but it illustrates the point. This assignment is safe as long as you only use the variable a to access the array and not store anything new in it. That is, a[i] is treated as Any and you can use any of the methods that Any has, and the underlying Int certainly has these. However, if you assign to an element of a then things are potentially more risky. You can assign any object you like as all objects are derived from Any. This leaves our Int array in something of a potential mess. Consider what happens if we assign a String to an element and then try and access it as if it was an Int. The result would be a run time exception and there is no way that this could be picked up at compile time. This is clearly not a good idea. You can also come to the conclusion that treating the array as contravariant isn't a good idea either. In this case the problem arises when you try and access an element of the array and treat it like a derived class of Int only to discover that it is just an Int. This is the reason why Kotlin defines an array as invariant and this makes the above code illegal as is any code that assigns an Array<T> variable to anything other than another Array<T>. Controlling Variance – in & outAll generics in Kotlin are, by default, invariant. That is, they are their own type and not related to anything else in the hierarchy. This is safe in that you can pick up type errors at compile time and you can't generate a run-time error by using the wrong type. However, it stops you doing things that are type safe. It all depends on whether the type parameter in question is used as an input, an output or both. The idea is that if a type is used only as an input then it is safe to treat the generic as contravariant. If it is only used as an output then it can be used covariantly and if it is both you have no choice but to treat the generic as invariant. Kotlin provides two modifiers, in and out, which allow you to mark type parameters as contravariant or covariant. Note: You can only use type variance modifiers on generic classes and interfaces, not in generic functions. To see how this works we need to create a sample generic class that has a read-only property: class MyClass<T>( myParam: T) { The reason for the strange implementation of a read-only property is that we want to convert it to a write-only property in a moment, and the usual way of implementing a property doesn't support this. By default this generic is invariant. What this means is that: var b=MyClass<Int>(1) is trying to treat MyClass<Int> as a derived class of MyClass<Any> produces a compile-time error: Similarly: var b=MyClass<Any>(1) That is, trying to treat MyClass<Any> as a derived class of MyClass<Int> also doesn't work in the same way. As our class has a read-only property, treating it as contravariant seems like a reasonable thing to do and we can by using the out modifier. class MyClass<out T>( myParam: T) { private var t:T=myParam fun read():T{ return t } } If the T type parameter is used anywhere in the class definition in a way that is an input, you will see a warning that you are using out incorrectly. With the out modifier we can now treat the class as covariant and: var b=MyClass<Int>(1) var a:MyClass<Any> a=b println(a.read()) works perfectly. Now if we change T to be an input only and add the in modifier: class MyClass<in T>( myParam: T) { we now have a contravariant generic class. This allows us to write: var b=MyClass<Any>(1) That’s all there is to the use of in and out – they simply select contravariant or covariant behavior for the generic on that parameter. This use of in and out when the generic is declared is called declaration-site variance, and it is useful when you are creating generics, but what about when you are just consuming them – use-site variance? <ASIN:1871962536> <ASIN:1871962544> |
||||
Last Updated ( Monday, 21 January 2019 ) |