``kt List``는 클래스(기저 클래스)이고, ``kt List<Int>``는 타입이다.

타입 ``kt A``의 값이 필요한 모든 장소에 타입 ``kt B``의 값을 넣어도 아무 문제가 없다면 ``kt B``는 ``kt A``의 하위 타입(subtype)이다.

하위 클래스와 하위 타입은 미묘한 차이가 있다. ``kt A?``와 ``kt A``는 같은 클래스에 속하지만, ``kt A``는 ``kt A?``의 하위 타입이고 그 역은 성립하지 않는다

 

무공변성, 불변성(invariance)

제네릭 타입을 인스턴스화할 때 서로 다른 타입 인자가 들어가는 경우 인스턴스 타입 사이의 하위 타입 관계가 성립하지 않으면 그 제네릭 타입을 무공변이라 한다.

e.g., ``kt MutableList<T>``에서 ``kt T``가 서로 다르다면 무조건 하위 타입 관계가 성립하지 않으므로 ``kt MutableList``는 무공변이다.

 

Note ) 자바와 코틀린 모두 따로 지정해주지 않으면 기본적으로 모든 제네릭 클래스는 무공변이다.

  • 선언 지점 변성 : 코틀린
  • 사용 지점 변성 : 코틀린, 자바

 

공변성(convariance) out : producer

타입 인자 사이의 하위 타입 관계가 성립하고, 그 하위 타입 관계가 그대로 인스턴스 타입 사이의 관계로 이어지는 경우 공변적이라 한다.

e.g., ``kt B``가 ``kt A``의 하위 타입일 때, ``kt List<B>``는 ``kt List<A>``의 하위 타입이므로 ``kt List``는 타입 인자 T에 대해 공변적이다.

 

공변적으로 선언해야 하는 상황은 다음과 같다. 다음과 같은 클래스와 함수들이 있을 때,

// 동물 1 개체를 의미
open class Animal {
    fun feed() { }
}

// Animal을 상속한 Cat
class Cat : Animal() { }

// 동물 무리 collection을 의미
class Herd<T: Animal> {
    val size: Int get() = TODO()
    operator fun get(i: Int): T { }
}

// 최상위 함수
fun feedAll(animals: Herd<Animal>) {
    for (i in 0 until animals.size) {
        animals[i].feed()
    }
}

``kt Cat``이 ``kt Animal``을 상속하기는 했지만 어떤 변성도 지정하지 않았기 때문에(무공변) ``kt Herd<Cat>``은 ``kt Herd<Animal>``의 하위 타입이 아니다.

* 물론 이를 강제 캐스팅으로 해결할 수는 있으나 그렇게 하는 것이 올바른 방법은 아니다.

```kt

>>> val cats = Herd<Cat>()

>>> feedAll(cats)    // Type mismatch.  require : Herd<Animal>

```

 

제네릭 클래스가 타입 파라미터에 대해 공변적임을 표시하려면 ``kt out``을 지정한다.

class Herd<out T: Animal>(vararg animals: T) {
    fun addAnimal(animal: T) { }     // in 위치라 이렇게는 사용할 수 없다.
    fun getBestAnimal(): T { }       // out 위치에는 사용 가능.

이는 다음 두 가지를 의미한다.

  1. 이제 ``kt feedAll()``은 ``kt Animal``의 하위 타입으로 이루어진 컬렉션도 받을 수 있다.
  2. ``kt out``이 지정된 공변적 파라미터는 out 위치(e.g., 리턴 타입)에만 사용할 수 있다. 
  3. 이는 ``kt T`` 타입의 값을 생산한다는 의미다. (생산이란?)
    1. 만약 in 위치의 사용을 제한하지 않는다면, ``kt addAnimal(tiger1)``도 가능하다는 얘기가 되므로 ``kt Herd<Cat>`` 이라는 컬렉션의 ``kt animals: Cat``에 Tiger가 들어가는 상황이 생길 수 있다. 

 

생성자 파라미터에는 in/out 위치 관계 없이 그냥 사용 가능하다. 이는 생성자의 경우 굳이 위치를 제한할 필요가 없기 때문이다. 

변성은 위험할 여지가 있는 메소드를 호출할 수 없게 만듦으로써 외부에서 제네릭 타입의 기저 클래스 인스턴스를 잘못 사용하는 일이 없도록 방지하는 역할인데, 생성자는 생성 시점에만 호출되는 메소드이므로 이런 방지 조치가 필요 없기 때문.

그러나 ``kt val / var``을 지정하는 경우 게터 세터가 같이 생성되기 때문에 이런 경우 in/out을 따져보아야 한다.

 

비공개 파라미터 메소드도 같은 맥락에서 in/out 위치 관계 없이 사용 가능하다. 외부에서 애초에 접근이 불가능하기 때문.

 

반공변성(contravariance) in : consumer

하위 타입 관계가 뒤집히면 반공변적이다. 
e.g., ``kt B``가 ``kt A``의 하위 타입일 때, ``kt Comparator<A>``는 ``kt Comparator<B>``의 하위 타입이므로 ``kt Comparator``는 타입 인자 ``kt T``에 대해 반공변적이다.
즉, ``kt Comparator<String>``을 요구하는 함수에 ``kt Comparator<Any>``를 넘길 수 있다는 것을 의미한다.
* 어떤 타입의 객체를 비교할 때 그 타입의 조상 타입을 비교할 수 있는 ``kt Comparator``를 사용할 수 있기 때문.
 
``kt in``이 지정된 반공변적 파라미터는 in 위치(e.g., 파라미터 타입)에만 사용할 수 있다.
이는 ``kt T`` 타입의 값을 소비한다는 의미다.
 

타입 프로젝션 ( type projection )

Note ) 어떤 클래스의 하위 타입 관계를 지정한다는 역할도 포함하고 있지만, 그 보다는 ``kt in``이 지정된 타입은 ``kt in``위치에서만 사용할 수 있도록 한다는, 데이터에 대한 수정/읽기 제한의 의미가 더 큰 듯 하다.
 
클래스를 선언하면서 클래스 자체에 변성을 지정하는 방식(클래스에 ``kt in/out``을 지정하는 방식)은 선언 지점 변성(declaration-site variance)이다. 선언 하면서 지정하면, 클래스의 공변성을 전체적으로 지정하는게 되기 때문에 클래스를 사용하는 장소에서는 따로 타입을 지정해줄 필요가 없어 편리하다.
 
반면 사용 지점 변성(use-site variance)메소드 파라미터에서, 또는 제네릭 클래스를 생성할 때 등 구체적인 사용 위치에서 변성을 지정하는 방식이다.
자바에서 사용하는 한정 와일드카드(bounded wildcard)가 바로 이 방식이다.
이는 타입 파라미터가 있는 타입을 사용할 때마다 해당 타입 파라미터를 하위 타입이나 상위 타입 중 어떤 타입으로 대치할 수 있는지를 명시해야 한다.
 

코틀린도 사용 지점 변성을 지원하며, 자바의 한정 와일드카드와 동일하다.

MutableList<out T> == MutableList<? extends T>
MutableList<in T>  == MutableList<? super T>
 
다음과 같은 상황에서, 
  1. `` src``가 `` dst``에 대한 하위 타입 호환성을 가지도록 만들고 싶으면서
  2. `` src``에 대한 수정을 방지하고 싶은 경우.
    이 경우 ``kt MutableList<T>``는 in/out 위치에서 모두 사용되기 때문에 ``kt out``을 지정해줄 수 없다.
Note ) 사실 2번 같은 경우 ``kt src: List<T>``로 구현하는게 best practice지만, 예제를 설명하기 위함.
fun <T> copyData(src: MutableList<T>,
                 dst: MutableList<T>) {
    for (item in src) {
        dst.add(item)
    }
}

1번만 해결해야 하고 2번이 필요 없는 경우, 즉 `` T``를 지정한 위치와는 반대되는 위치에 사용하는 메소드도 호출해야 한다면 다음과 같이 타입 파라미터를 하나 더 쓰는 방법으로 작성하는게 좋다.

fun <T: R, R> copyData(src: MutableList<T>,
                       dst: MutableList<R>) {
    for (item in src) {
        dst.add(item)
    }
}

 

사용 지점 변성을 사용하면 1, 2를 모두 만족하면서 더 안전하게 처리할 수 있다. ( C#도 같은 방식이다. )

fun <T> copyData(src: MutableList<out T>,
                 dst: MutableList<T>) {
    for (item in src) {
        dst.add(item)
    }
}

이렇게 지정하면 `` src``의 타입이 `` MutableList``를 프로젝션한(제약을 가한) 타입이 된다. 이를 타입 프로젝션이라 한다.

따라서 이 경우 `` src``는 `` MutableList``의 메소드 중 타입 파라미터 `` T``를 ``kt in`` 위치에 사용하는 메소드는 사용할 수 없다.

 

이런 식으로 제너릭 클래스 생성 시점에도 변성``kt in/out``을 지정할 수 있다.

>>> val list: MutableList<out Number> = ...  
>>> list.add(1)
Error: Out-projected type ....

 

이런식으로 사용할 수 있는 위치를 제한하는 이유는, type safety를 보장하기 위해서다.

예를 들어, ``kt Number`` 타입의 하위 타입이면서 서로 같은 두 인자를 받고 싶을 때 이런 식으로 코딩하게 되면 예상과 다르게 동작한다. 

```java

public static void copyData(List<? extends Number> src, List<? extends Number> dst)

```

이런 코드는 `` src``의 타입과 `` dst``의 타입이 같으리라고 보장할 수 없기 때문에 type safe하지 않다.

코틀린으로 위와 같은 코드를 짜려고 하면, ``java <? extends T>``와 대응되는 것은 ``kt <out T>``이므로 `` dst``에 이를 지정하는 순간 에러가 발생해 다른 식으로 접근해야 한다는 것을 알려준다. 

(이 경우 `` src``만 ``kt out``으로 지정하는게 올바른 방법이다.)

또한 자바에서는 ``java ?``를 사용해도 되고 `` T``를 사용해도 되는 상황에서 어떤 것을 선택할지를 유저에게 맡겼는데, 코틀린은 이를 조금 더 빡빡하게 제한해 일관성있는 코드를 작성할 수 있다.

* 보통 자바에서 ``java ?``와 `` T``를 모두 사용할 수 있을 때 인자들과 리턴타입이 서로 관계를 맺고 있으면 `` T``, 아니면 ``java ?``를 사용해야 조금 더 의도가 명확해지는 코드가 된다.

 

스타 프로젝션 : *

1. *와 Any?는 다르다.

왜냐면, ``kt MutableList<T>``는 `` T``에 대해 무공변이기 때문.

``kt MutableList<Any?>``는 모든 타입의 원소를 담을 수 있음을 의미하는 반면,

``kt MutableList<*>``은 어떤 타입이라도 들어올 수 있으나, 구체적인 타입이 결정되는 과정이 진행되고, 일단 타입이 결정되면 그 타입(과 하위 타입)의 원소만 담을 수 있다. 즉, 구체적인 타입이 결정된다는 점이 중요하다.

 

2. *와 자바의 ?는 다르다.

MutableList<*>  == MutableList<?>

자바로 표현하면 위와 같겠으나, bounded wildcard는 사실 ``kt in/out``에 대응되기 때문에, unbounded만 표현한다고 볼 수 있다.

게다가, 코틀린 컴파일러는 ``kt <*>``를 맥락에 따라서 해석한다.

그러니까, 다음과 같이 정의되어 있는 인터페이스에 대해서 

interface Function(in T, out U> {
    . . .
}

이 인터페이스를 구현한 것들을 인자로 받고 싶다면 다음과 같이 적을텐데,

fun foo(bar: Function<*, *>) {
    // == bar: Function<in Nothing, out Any?>
}

``kt in``으로 정의되어 있는 인자를 ``kt *``로 받으면 ``kt in Nothing``인 것으로 간주한다.

``kt out``으로 정의되어 있는 인자를 ``kt *``로 받으면 ``kt out Any?``인 것으로 간주한다.

그래서 ``kt *``를 사용하더라도 함수 내부에서 `` T, U``의 위치에 따라 메소드 호출이 제한될 수 있다.

class VarianceTest<in T, out U>(conT: T, conU: U) {
//    val propT: T = conT
//    type parameter T is declared as 'in' but occurs in 'out' position in type T
    val propU: U = conU

//    fun printAll(t: T, u: U)
//    type parameter U is declared as 'out' but occurs in 'in' position in type U
    fun printAll(t: T) {
        print(t)
    }
}

fun starTestFunc(v: VarianceTest<*, *>) {
//    v.printAll(1)
//    Out-projected type 'VarianceTest<*, *>' prohibits the use of 'public final fun printAll(t: T): Unit defined in VarianceTest'
//    type hinting도 아예 v.printAll(t: Nothing)으로 잡힌다.
    print(v.propU)    // out은 out Any?가 되니까 Any?에 있는 메소드를 호출하거나 하는건 잘 된다.
}