TIL-15 # Kotlin “in”, “out”, “where” ## 제네릭 제네릭 프로그래밍(영어: generic programming)은 데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있는 기술에 중점을 두어 재사용성을 높일 수 있는 프로그래밍 방식입니다. 제네릭을 사용하다 보면 “in”, “out” “where”이라는 키워드를 보신적이 있으실 겁니다. 어떠한 차이점이 있으며, 왜 사용하는 걸까요? ## 문제점 이해하기 쉬운 예제가 있어서 가져왔습니다. (출처 : https://hungseong.tistory.com/30 ) “Animal”이라는 클래스가 있다고 가정해 보고 “Cat”, “Dog” 클래스는 “Animal” 클래스를 상속받고 있다고 해보겠습니다. ```kotlin open class Animal class Cat : Animal() class Dog : Animal() ``` 여기서 Array<T>와 같이 제네릭 타입을 가지는 클래스의 경우, 아래 코드 실행 시 컴파일 에러가 발생합니다. ```kotlin val dogs: Array<Dog> = arrayOf(Dog(), Dog()) // Error - Type mismatch: inferred type is Array<Dog> but Array<Animal> was expected val animals: Array<Aniaml> = dogs ``` 왜 이러한 에러가 발생했을까요? 기본적으로 클래스의 상속관계가 제네릭에서는 상속 관계가 유지되지 않는 Invariance(불변성)가 존재합니다. 즉, A가 B를 상속받아도 Class<A>는 Class<B>를 상속받지 않는다는 뜻입니다. 만약에 허용이 된다면 어떠한 문제가 있을까요? 아래의 예시를 보면 이해가 될겁니다. ```kotlin fun myAnimals(animals: Array<Animal>) { animals[0] = Cat() // Error - Dog의 Array인데 Cat이 들어가게 된다? } fun main() { val dogs: Array<Dog> = arrayOf(Dog(), Dog()) myAnimals(dogs) } ``` 여기서 Dog의 Array인데 Cat이 대입되는 불상사가 생기게 됩니다. 이를 방지하기 위해 Invariance(불변성)가 존재하게됩니다. 하지만 신기하게 아래의 예제를 보게 되면 에러가 발생하지 않습니다. ```kotlin fun myAnimals(animals: List<Animal>) { println(animals[0]) } fun main() { val dogs: Array<Dog> = arrayOf(Dog(), Dog()) myAnimals(dogs) } ``` 달라진 거는 “Array”가 “List”로 된것 뿐인데 왜 가능하게 된 걸까요? 키워드는 “Invariance(불변성)”에 있습니다. Array의 경우는 안에 내용물을 바꿀 수 있는 “가변” 이며 List의 경우에는 안에 내용물을 바꿀 수 없는 “불변”이기 떄문입니다. 두 개의 차이를 좀 더 살펴 보자면 아래의 코드와 같은 차이점이 있습니다. ```kotlin public class Array<T> public interface List<out E> ``` 여기서 “out”이라는 키워드가 나왔습니다. 어떠한 역할을 하는 걸까요? ## out - 자바에서 “? extends E”와 유사한 역할을 하며 covariance(공변)이라고 불립니다. Invariance(불변성)이 코드의 안정성을 높여주지만 필요하지 않은 상황이 올 수도 있습니다. ```kotlin fun copyFromTo(from: Array<Animal>, to: Array<Animal>) { for (i in from.indices) { to[i] = from[i] } } fun main() { val animals: Array<Animal> = arrayOf(Animal(), Animal()) val dogs: Array<Dog> = arrayOf(Dog(), Dog()) // Error - Type mismatch: inferred type is Array<Dog> but Array<Animal> was expected copyFromTo(dogs, animals) } ``` 개의 배열을 동물의 배열로 복사한다고 가정했을 때, 안타깝게도 이 코드는 에러가 발생합니다. A가 B를 상속 받았을때 Class<A>가 Class<B>를 상속받지 않았다는 불변성의 원리로 인해 에러가 발생했기 때문입니다. 이를 해결하기 위해 가 B를 상속 받았을때 Class<A>가 Class<B>를 상속받았다고 바꿔줘야 하며 이를 “covariance(공변)”이라고 합니다. 이를 해결하기 위한 키워든 “out”이며 적용을 해보면 아래와 같습니다. ```kotlin fun copyFromTo(from: Array<out Animal>, to: Array<Animal>) { for (i in from.indices) { to[i] = from[i] } } fun main() { val animals: Array<Animal> = arrayOf(Animal(), Animal()) val dogs: Array<Dog> = arrayOf(Dog(), Dog()) copyFromTo(dogs, animals) } ``` out으로 선언해버리면 읽는 것은 가능하지만 from에 값을 대입하는 것과 같이 쓰는 것은 불가능해집니다. 이렇게 하면 컴파일러는 from의 부모가 “Animal” 이라는 사실을 알게 되며, 모두를 포함하는 “Animal”로 할당하기 때문에 읽는데는 문제가 안 생기게 됩니다. ## in - 자바에서 “? super E”와 유사한 역학을 하며 contravariance(반공변성)이라고 불립니다. 만약에 Animal의 상위 타입의 Array에 값을 복사한다고 했을때 어떻게 될까요? ```kotlin fun copyFromTo(from: Array<out Animal>, to: Array<Animal>) { for (i in from.indices) { to[i] = from[i] } } fun main() { val anys: Array<Any> = arrayOf(Any(), Any()) val dogs: Array<Dog> = arrayOf(Dog(), Dog()) copyFromTo(dogs, anys) } ``` 상위 타입의 Array이므로, 문제없는 코드로 보입니다. 하지만 컴파일 에러가 발생하고 맙니다. 왜 그럴까요? to에서 값을 하나 뽑아서 Animal에 대입한다고 했을 때 아래와 같은 오류가 발생할 수 있습니다. ```kotlin fun copyFromTo(from: Array<out Animal>, to: Array<Animal>) { for (i in from.indices) { to[i] = from[i] } val animal: Animal = to[0] // Any but Animal?! } fun main() { val anys: Array<Any> = arrayOf(Any(), Any()) val dogs: Array<Dog> = arrayOf(Dog(), Dog()) copyFromTo(dogs, anys) } ``` Any 타입인데 Animal 타입으로 뽑게 되는 문제가 생기며 이런 일이 발생하지 않게 불변성에 의해 컴파일러 에러가 발생하게 됩니다. 이를 방지하기 위해서는 to를 쓰기만 가능하도록 하면 될 것 같습니다. 그래서 나온 키워드는 “in”입니다. “in”을 사용하면서 A가 B를 상속받으면 Class<B>는 Class<A>를 상속받는다는 관계가 성립하게 됩니다. 즉 contravariance(반공변성)이 됩니다. ```kotlin fun copyFromTo(from: Array<out Animal>, to: Array<in Animal>) { for (i in from.indices) { to[i] = from[i] } } fun main() { val anys: Array<Any> = arrayOf(Any(), Any()) val dogs: Array<Dog> = arrayOf(Dog(), Dog()) copyFromTo(dogs, anys) } ``` to는 Animal의 상위 타입만 들어갈 수 있게 되며 쓰는것 밖에 안되게 됩니다. ## where 제네릭 타입을 제한하고 싶을 경우도 있을 겁니다. 하나의 타입으로 제한을 하고 싶으면 아래와 같이 제한이 가능합니다. ```kotlin fun <T: SomeClassOrInterface> foo(input: T) { input.someFunction() } ``` 만약에 하나 이상의 타입으로 제한하고 싶으면 어떻게 하면 될까요? “where”라는 키워드를 사용하면 가능합니다. ```kotlin fun <T> foo(input: T) where T: SomeClassOrInterfaceOne, T: SomeClassOrInterfaceOneTwo { input.someFunctionOne() input.someFunctionTwo() } ``` ## Star-projection “Star-projection(*)” 은 제네릭 타입에서 일반적인 타입을 알지 못할 때 사용됩니다. covariant 는 *를 만나면 out Any? 로 변하고, contravariant는 *를 만나면 in Nothing 로 변합니다. 즉 out에서는 쓰기를 못하며 in 에서는 읽기를 하지 못하게 됩니다. - `Function<*, String>` →`Function<in Nothing, String>`. - `Function<Int, *>` → `Function<Int, out Any?>`. - `Function<*, *>` → `Function<in Nothing, out Any?>`. ## reified 제네릭은 코드를 컴파일할 때에는 제네릭타입이 어떤건지 알고 있지만, 컴파일하면서 타입 정보를 제거하여 Runtime에서는 제네릭타입이 어떠한 타입인지 모르게됩니다. 코틀린에서는 이를 해결하기위해서 “reified(구체화된)”이라는 키워드를 제공해주고 있습니다. 해당 키워드는 inline function에서만 사용이 가능하며, 아래와 같이 사용가능합니다. ```kotlin fun <T> foo(value: T, classType: Class<T>) { when (classType) { String::class.java -> { println("String : $value") } Int::class.java -> { println("Int : $value") } } } // using reified inline fun <reified T> foo(value: T) { when (T::class) { String::class -> { println("String : $value") } Int::class -> { println("Int : $value") } } } ``` 참고 - https://kotlinlang.org/docs/generics.html#generic-functions - https://hungseong.tistory.com/30
로그인 후 모든 글을 볼 수 있습니다.