본문 바로가기
프로그래밍 놀이터/Kotlin, Coroutine

[Effective Kotlin] Item 24 : Consider variance for generic types

by 돼지왕 왕돼지 2022. 3. 21.
반응형

이 글은 Effective Java 를 완독하고, Kotlin 을 상용으로 사용하는 개발자 입장에서
Effective Kotlin 글 중 새로운 내용, remind 할 필요 있는 부분, 핵심 내용 등만 추려 정리한 내용입니다.

 

#

class Cup<T>

variance modifier (out, in) 마킹이 없으면 invariant 이다.
이 말은 Cup<Any>, Cup<Int> 는 서로 다른 2개의 타입으로 정의된다. Int 가 Any 을 상속했음에도

 

#
variance modifier 를 사용해 관계를 지정해줄 수 있다.

out 은 covariant 를 지정한다. A extends B 라면 Cup<A> 가 Cup<B> 의 subtype 이다.
즉 val anys: Cup<Any> = Cup<Int>() 가 가능하다.

in 은 contravariant 를 지정한다. A extends B 라면 Cup<A> 가 Cup<B> 의 supertype 이다.
즉 val anys: Cup<Any> = Cup<Int>() 는 불가능하고,
val nothings:Cup<Nothing> = Cup<Int>() 는 가능하다.

 

 

Function types

#

fun printProcessedNumber(transition: (Int)->Any){
	print(transition(42))
}

(Int)->Any 자리에 (Int)->Number, (Number)->Any, (Number)->Number, (Any)->Number, (Number)->Int 등이 모두 올 수 있다.

 

#
Kotlin function type 의 모든 parameter type 은 contravariant 이다.
그리고 모든 Kotlin function type 의 모든 return type 은 covariant 이다.

(T1in, T2in) -> Tout

그러므로 (Int)->Any 에 대해 Int 의 대체는 super class 쪽으로 뻗어나가고, Any 는 sub class 쪽으로 뻗어나간다.

 

#
List 는 covariant 하고, MutableList 는 invariant 하다.
Array 도 invariant 하다.

 

 

The safety of variance modifiers

#
out 으로 표기된 typed param 은 in (param 으로 받기) position 에 올 수 없다.
그래서 covariant 한 녀석은 producer 나 immutable 에 사용된다.

in 으로 표기된 typed param 은 out (return 으로 전달하기) position 에 올 수 없다.
그래서 contravariant 한 녀석은 consumer 나 accepter 에만 사용된다.

단, private 마킹과 primary constructor 에 한해 이 rule 을 어길 수 있다.

 

 

Variance modifier positions

#
variance modifier 는 두 위치에 올 수 있다.
더 일반적인 declaration-side 와 class or interface 정의에 사용하는 use-side 이다.

 

#
use-site variance 는 모든 instance 에 대해 variance modifier 를 제공할 수 없지만, 필요한 경우에 사용된다.

class Box<out T>(val value: T) // declaration-side

class Box<T>(val value: T)
val boxStr: Box<String> = Box("Str")
val boxAny: Box<out Any> = boxStr // use-side

 

#

interface Dog
interface Cutie
data class Puppy(val name: String): Dog, Cutie
data class Hound(val name: String): Dog
data class Cat(val name: String): Cutie

fun fillWithPuppies(list: MutableList<in Puppy>){
	list.add(Puppy("Jim"))
	list.add(Puppy("Beam"))
}

val dogs = mutableListOf<Dog>(Hound("Pluto"))
fillWithPuppies(dogs)
println(dogs) // Hound(Pluto), Puppy(Jim), Puppy(Beam)

val animals = mutableListOf<Cutie>(Cat("Felix"))
fillWithPuppies(animals)
println(animals) // Cat(Felix), Puppy(Jim), Puppy(Beam)

 

#

MutableList<out T> 를 사용하는 경우 get 만 사용 가능하고, T type 을 return 받는다.
set 의 경우 Nothing 이 기대되기에 사용할 수 없다. Nothing 이 기대되는 이유는 Nothing 이 모든 type 의 sub type 이기 때문이다.

MutableList<in T> 를 사용하는 경우 get, set 모두 가능하지만, get 을 하면 returned type 은 Any? 가 된다. 이는 모든 type 의 최종 super type 이 Any? 이기 때문이다.

그러므로 read 만 할 경우는 out, write 만 할 경우는 in 을 쓰면 된다.

 

 

Summary

#
A extends B 일 때,
기본 variance 동작은 invariance. Cup<A> 와 Cup<B> 는 아무 상관 없음.
out modifier 는 covariant 로 만드는데, Cup<A> 는 Cup<B> 의 sub class. 그리고 type parameter 는 out pos (return) 에만 올 수 있다.
in modifier 는 contravariant 로 만드는데, Cup<A> 는 Cup<B> 의 super class. 그리고 type parameter 는 in pos (param) 에만 올 수 있다.

 

#
Kotlin 에서 List, Set, Map 은 covariant 이다. (out modifier)
Array, MutableList, MutableSet, MutableMap 은 invariant 이다. (no modifier)

 

#
Function type 에서 param 쪽은 contravariant (in modifier) 이고, return type 쪽은 covariant (out modifier) 이다.

 

 

 

반응형

댓글