Kotlin Generics: Covariance, Contravariance, Invariance

794 단어·2 분·원문(.md)

Variance? #

In Kotlin, variance refers to how type parameters affect class hierarchies.

For example, if a value of type B can be used anywhere a value of type A is required without issues, then B is a subtype of A.

val integer: Int = 1
val number: Number = integer // Number is a supertype, Int is a subtype

We can see that an integer variable declared as Int can be assigned to a number variable declared as Number without any problems.

This is variance. Another example is Int? and Int. Since Int? includes null, Int? becomes the supertype of Int.

Three Types of Variance #

Generics fundamentally lack a supertype/subtype concept between classes, making them unrelated.

What if we want to establish types based on supertype and subtype relationships? This is where a new concept called variance comes into play.

There are three types of variance: covariance, contravariance, and invariance. Let's explore them.

Covariance #

First, it's clear that Int is a subtype of Number. When defined as Class Box<out T>, Class Box<Int> becomes a subtype of Class Box<Number>.

You can simply understand it as is. Covariance carries over the supertype/subtype concept of type parameters.

If A is a subtype of B, then Class<A> is a subtype of Class<B>.

out property from the producer's perspective.

Contravariance #

This is the opposite concept of covariance.

It's fundamentally clear that Int is a subtype of Number. When defined as Class Box<in T>, Class Box<Number> becomes a subtype of Class Box<Int>.

If A is a subtype of B, then Class<B> is a subtype of Class<A>.

in property from the consumer's perspective.

Invariance #

Invariance is the concept that there is no relationship between two classes.

Class<A> and Class<B> have no relationship.

Producer + Consumer

Invariance Example #

When instantiating generic classes, you must carefully consider the supertype/subtype relationships between data types if you want to use different types as arguments.

If you declare a type parameter without the out or in keywords, the class is declared as invariant.

Looking at the relationship between Nothing, Int, and Any types, they have a supertype/subtype relationship: Nothing <-> Int <-> Any. Even with such a clear hierarchy, if a class is declared as invariant, it will cause a type mismatch error.

class Box<T>(val size: Int) // 무변성으로 제네릭 클래스 선언

fun main(){
    val anys: Box<Any> = Box<Int>(10) // 오류, 자료형 불일치
    val nothings: Box<Nothing> = Box<Int>(19) // 오류, 자료형 불일치
}

Covariance Example #

When the supertype/subtype relationship of type parameters holds true, and that relationship directly extends to the instance types, this is called covariance.

class Box<out T>(val size: Int) // 공변성으로 제네릭 클래스 선언

fun main() {
    val anys: Box<Any> = Box<Int>(10) // 객체 생성 가능!
    val nothings: Box<Nothing> = Box<Int>(10) // 오류, 자료형 불일치
}

The class was declared as covariant using the out keyword.

In the case of Any, since it is a superclass of Int, an object created with type Int can also be included in Any.

However, in the case of Nothing, since it is a subclass of Int, an object created with type Int cannot be included. (Perhaps think of it like regular inheritance.)

Restricting Types in Covariance #

We previously discussed that you can restrict data types when using generics. The same applies to covariance.

open class Animal(val size: Int){
    fun feed() = println("냠냠냠냠")
}

class Cat(val jump: Int): Animal(50)
class Dog(val weight: Int): Animal(80)

class Box<out T: Animal>(val element: T){
    // out 키워드를 사용해 공변성으로 선언
    // 따라서 하위 자료형이 상위 자료형으로 변환하는 것은 괜찮다.
}

fun main() {
    val c1: Cat = Cat(10)
    val c2: Dog = Dog(20)

    var a1: Animal = c1 // 하위 자료형이니 상위 자료형으로 변환 가능

    val c3: Box<Cat> = Box<Animal>(10) // 상위 자료형이 하위 자료형으로의 변환은 불가능
    val c4: Box<Animal> = Box<Cat>(10) // 자료형을 Animal로 제한했으므로 Animal, Cat, Dog를 제외한 다른 자료형은 사용 불가능.
}
Back-End/kotlin/공변성.md