아이템 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랑 관계가 없다.
}
'스터디 > 이펙티브코틀린' 카테고리의 다른 글
아이템 30 - 요소의 가시성을 최소화화라 (0) | 2023.06.12 |
---|---|
아이템 23 - 타입 파라미터의 섀도잉을 피하라 (0) | 2023.05.14 |
아이템 21 - 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라 (0) | 2023.05.14 |
아이템 19 - knowledge를 반복하여 사용하지 말라 (0) | 2023.05.03 |
아이템 17 - 이름 있는 아규먼트를 사용하라 (0) | 2023.04.27 |