아이템 24 - 제네릭 타입과 variance 한정자를 활용하라

ds_chanin

·

2023. 5. 14. 22:59


제네릭 타입(T)에 한정자 out과 in이 붙은 경우 다음과 같이 생각하자.

  • out : T는 반환 타입으로만 사용할 수 있다.
  • in: T는 타입 매개변수로만 사용할 수 있다.

variance: 가변성, 차이

여기서 variance 한정자는 out과 in 키워드를 의미한다.

 

out: 공변성(covariant)으로 만든다. 자바의 extends와 유사하다. 상한을 지정한다.

out 한정자를 사용하면서 발생하는 차이는 아래 코드로 설명한다.

class Cup<T>

fun main() {
  val anys: Cup<Any> = Cup<Int> // 컴파일 에러, 서로 관련이 없는것으로 취급
}


class Cup<out T>

fun main() {
  val anys: Cup<Any> = Cup<Int> // Any를 기반으로 하는 모든 타입을 허용
}

 

in: 반변성(contravariant)으로 만든다. 자바의 super와 유사하다. 하한을 지정한다.

in 한정자에 대한 설명은 다음 코드와 같다.

class Cup<in T>

open class RoundCup // 원형
open class OvalCup(): RoundCup() // 타원형

fun main() {
  val roundCup: Cup<RoundCup> = Cup<OvalCup>() // 컴파일 에러
  val ovalCup: Cup<OvalCup> = Cup<RoundCup>() // 통과
}

 

코틀린의 함수 타입의 모든 파라미터는 반변성(contravariant)을 가진다. 즉, 하한이 지정되어 있다. 

반면 리턴타입은 공변성(covariant)을 가지므로 상한이 지정되어 있다.

 

아래와 같이 사용할 수 있다는 말이다.

interface Big
interface Medium : Big
interface Small : Medium

class BigImpl() : Big
class MediumImpl() : Medium
class SmallImpl() : Small

fun invoke(function: (Medium) -> Medium) {}

fun main() {
  invoke { it: Big -> // medium이 하한선이니 medium 혹은 big이 될 수 있다.
    SmallImpl() // medium이 상한선이니 small 혹은 medium이 될 수 있다. 
  }
}

 

자바의 배열은 공변성(covariant)이다. 상한선을 가지고 있다는 말이다.

그래서 아래와 같이 코드를 작성해도 컴파일 에러는 발생하지 않지만 런타임에 에러가 발생한다.

interface Medium : Big
interface Small : Medium
interface Tiny : Medium

class SmallImpl() : Small
class TinyImpl() : Tiny

public static void main(String[] args) {
  Medium[] mediums;
  Small[] smalls = {new SmallImpl()};
  Tiny[] tinies = {new TinyImpl()};

  mediums = smalls;
  mediums[0] = tinies[0]; // runtime에 ArrayStoreException 발생
}

그래서 코틀린은 Array를 불변(invariant)하도록 만들었다.

그래서 자바처럼 아래와 작성하면 컴파일 에러가 발생한다.

fun main() {
  var mediums: Array<Medium>
  val smalls = arrayOf<Small>(SmallImpl())
  val tinies = arrayOf<Tiny>(TinyImpl())

  mediums = smalls // 컴파일 에러 Type missmatch
}

 

 

함수에 선언된 파라미터는 기본적으로 in 한정자 파라미터이다.

in이 아닌 out으로 타입 파라미터의 상한선을 지정한 경우 아래와 같이 set의 파라미터에 T가 위치하는 코드를 작성 할 수 없다.

class Size<out T> {
  private var value: T? = null // 가시성 제한(private)을 풀면 여기서도 컴파일 에러 발생
    
  fun set(value: T) { // 컴파일 에러(Type parameter T is declared as 'out' but occurs in 'in' position in type T)
      this.value = value 
  }
}

위 코드가 가능하면 아래와 같은 코드가 작성이 가능해지기 때문이다.

fun main() {
  val size: Size<Medium> = Size<Small>() // out 이니까 가능하다.
  size.set(Tiny()) // Small하고 Tiny는 아무런 관계가 없어서 이렇게 되어선 안된다.
}

 

in은 out과 반대되는 상황에서 안된다.

class Size<in T> {
  private var value: T? = null // 가시성 제한(private)을 풀면 여기서도 컴파일 에러 발생
    
  fun get(): T { // 컴파일 에러(Type parameter T is declared as 'in' but occurs in 'out' position in type T)
      return value
  }
}


fun main() {
  val small: Size<Small> = Size<Small>()
  val medium: Size<Medium> = small // in 이니까 가능한데
  val tiny: Tiny = medium.value // 이 부분이 불가능 하기 때문이다. small은 tiny랑 관계가 없다.
}