관리 메뉴

나만을 위한 블로그

[Kotlin] 무공변성, 공변성, 반공변성과 in / out 본문

개인 공부/Kotlin

[Kotlin] 무공변성, 공변성, 반공변성과 in / out

참깨빵위에참깨빵_ 2023. 2. 22. 23:01
728x90
반응형

깃허브에서 안드로이드 소스코드를 보다 보면 가끔 in, out, where, reified 키워드를 볼 수 있다.

이 중 in, out은 제네릭의 공변성, 반공변성 개념과 관련된 키워드라서 개념 이름부터 무슨 말인지 몰랐었다. 그래서 정리하고자 포스팅한다.

 

먼저 위키백과의 공변성, 반공변성 문서는 아래와 같다.

 

https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science) 

 

Covariance and contravariance (computer science) - Wikipedia

From Wikipedia, the free encyclopedia Many programming language type systems support subtyping. For instance, if the type Cat is a subtype of Animal, then an expression of type Cat should be substitutable wherever an expression of type Animal is used. Vari

en.wikipedia.org

많은 프로그래밍 언어 타입 시스템은 하위 타입 지정을 지원한다. Cat 타입이 Animal의 하위 타입인 경우 Cat 타입의 표현식은 Animal 타입의 표현식이 쓰일 때마다 대체 가능해야 한다. 공변(Variance)은 보다 복잡한 타입 간의 하위 타입이 해당 구성요소 간의 하위 타입과 어떻게 관련되는지를 나타낸다. 고양이 목록이 동물 목록과 어떤 관련이 있어야 하는가? 또는 Cat을 반환하는 함수는 Animal을 반환하는 함수와 어떻게 관련되어야 하는가? 타입 생성자의 공변에 따라 단순 타입의 하위 타입 지정 관계는 각각의 복합 유형에 대해 보존, 반전 또는 무시될 수 있다. 예를 들어 "list Of Cat"은 "list Of Animal"의 하위 타입이다. 리스트 타입 생성자가 공변이기 때문이다. 이는 단순 타입의 하위 타입 지정 관계가 복합 타입에 대해 보존됨을 의미한다
반면 "function from Animal to String"은 "function from Cat to String"의 하위 타입이다. 함수 타입 생성자가 매개변수 타입에서 반공변이기 때문이다. 여기서 단순 타입의 하위 타입 지정 관계는 복합 타입에 대해 반전된다
즉 공변은 보다 구체적이어서 다른 특성(Cat은 Animal에 대해 공변이다)인 반면 반공변성은 보다 일반적이기 때문에 다른 특성이다(Animal은 Cat의 반공변이다)...(중략)

 

https://ko.wikipedia.org/wiki/%EA%B3%B5%EB%B3%80%EC%84%B1%EA%B3%BC_%EB%B0%98%EA%B3%B5%EB%B3%80%EC%84%B1_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99) 

 

공변성과 반공변성 (컴퓨터 과학) - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 프로그래밍 언어의 공변성(영어: Covariance)과 반공변성(영어: Contravariance)은 프로그래밍 언어가 타입 생성자(영어: type constructor)에 있어 서브타입을 처리하는 방

ko.wikipedia.org

공변성, 반공변성은 프로그래밍 언어가 타입 생성자에 있어 서브타입을 처리하는 방법을 나타내는 것으로, 더 복잡한 타입 간의 서브타입 관계가 타입 사이의 서브타입 관계에 따라 정의되거나 이에 배반해 정의됨을 가리킨다
예를 들어 리스트와 함수에 대해 생각해 본다. 고양이는 동물의 서브타입이다. 그럼 고양이 목록은 동물 목록과 어떻게 연관되어야 할까? 또는 고양이를 인자로 받는 함수가 동물을 인자로 받는 함수와 어떻게 연관되어야 할까? 이는 변성(variance)으로 설명될 수 있다. 고양이 목록은 동물 목록의 서브타입이다. 목록 타입 생성자가 공변하기 때문이다. 그러나 동물 -> 문자열 함수는 고양이 -> 문자열 함수의 서브타입이다. 함수 타입 생성자는 인자 타입에 있어 반공변하기 때문이다

공변할 때 S가 T의 서브타입이면 I<S>는 I<T>의 서브타입이다
반공변할 때 S가 T의 서브타입이면 I<T>는 I<S>의 서브타입이다

 

예시들은 이해가 되는 것 같기도 한데 정의들이 이해가 안 된다. 다른 사람들이 설명해 놓은 글을 확인해 본다.

 

https://medium.com/mj-studio/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%A0%9C%EB%84%A4%EB%A6%AD-in-out-3b809869610e

 

코틀린 제네릭, in? out?

JVM 기반 언어인 Java와 Kotlin의 와일드카드와 불변(invariance), 공변(covariance), 반변(contravariance)에 대해

medium.com

자바에서 String은 Object의 서브타입이다. 그러나 List<String>은 List<Object>의 서브타입이 아니다. 왜 이런 제약을 만들었는가? 그렇지 않으면 다음 문제가 발생하기 때문이다
val strs: MutableList<String> = mutableListOf()
// 컴파일 에러가 발생합니다
val objs: MutableList<Object> = strs
objs.add(Object())
// 문자열이 아닌 Object 객체 가 들어있어서 런타임 에러가 날 것입니다
val str: String = strs[0]
만약 MutableList<String>이 MutableList<Object>의 서브타입이면 2번 줄에 에러가 발생하지 않아야 한다. 그러나 그렇게 되면 4번 줄이 런타임에 실행될 때 String이 아닌 Object가 반환되어 str.subString 따위를 호출하면 바로 런타임 에러가 발생하게 된다
이렇게 형식 인자들끼리는 서브타입 관계를 만족하더라도 제네릭을 쓰는 클래스, 인터페이스에선 서브타입 관계가 유지되지 않는 것이 무공변(=불변, Invariance)이다. 기본적으로 코틀린의 모든 제네릭에서의 형식 인자는 Invariance다
Invariance는 컴파일 타임 에러를 잡아주고 런타임 에러를 내지 않는 안전한 방법이다. 그러나 이는 가끔 안전하다고 보장된 상황에서도 컴파일 에러를 내서 개발자를 불편하게 할 수 있다...(중략)...이를 해결하기 위해 자바에선 와일드카드가 등장한다. 제네릭 형식 인자 선언에 "? extends E" 같은 문법을 통해 E나 E의 서브타입의 제네릭 형식을 전달받아 사용할 수 있다
// Java
interface Collection<E> ... {
  void addAll(Collection<? extends E> items);
}
여기서 짚고 넘어가야 할 것은 위 코드의 items에서 E를 읽을 순 있지만 items에 어떤 아이템도 추가할 수 없다. items에서 어떤 아이템을 꺼내든 E라는 형식 안에 담길 수 있지만, items에 어떤 값을 추가하려면 items의 형식 인자인 ?가 뭔지를 알아야 한다. 모르기 때문에 쓸 수가 없다...(중략)...읽기만 가능하고 쓰기는 불가능한 "? extends E"는 코틀린에서의 out과 비슷한 의미로 쓰이고 이런 것들을 공변(covariance)이라 부른다. 반대로 읽기는 불가능하고 쓰기는 가능한 자바에선 "? super E"로 쓰이고 코틀린에선 in으로 쓰이는 contravariance(반공변)이 있다. contravariance에선 E와 같거나 E보다 상위 타입만 ? 자리에 들어올 수 있다. items가 Collection<? super E>라면 items에서 어떤 변수를 꺼내도 E에 담을 수 있을지 보장할 수 없고(읽기 불가), E의 상위 타입 어느 것이든 items에 넣을 수 있기 때문에 covariance와 반대된다는 걸 이해하면 된다...(중략)

 

https://proandroiddev.com/understanding-type-variance-in-kotlin-d12ad566241b

 

Understanding type variance in Kotlin

In this post, I want to give you a clear explanation of type variance in Kotlin.

proandroiddev.com

< 무공변 >

- 제네릭 클래스는 서로 다른 두 타입 A, B에 대해 Class<A>가 Class<B>의 하위 타입도 아니고 상위 타입도 아닌 경우 타입 매개변수에 대해 무공변이라고 한다

Cat이 Any의 하위 타입이더라도 MutableList<Any>가 예상될 때 MutableList<Cat>을 전달하는 게 항상 안전하진 않다. 반대도 마찬가지다. MutableList<Any>는 MutableList<Cat>의 하위 타입이 아니다. 이런 클래스 또는 인터페이스를 무공변이라고 한다

< 공변(보존된 하위 타입 관계) >

- A가 B의 하위 타입인 경우 Class<A>는 Class<B>의 하위 타입이다

코틀린의 List 인터페이스는 읽기 전용 컬렉션이다. 즉 Cat이 Animal의 하위 타입이면 List<Cat>은 List<Animal>의 하위 타입이다. 이런 클래스 또는 인터페이스를 공변이라고 한다. out 한정자는 클래스를 특정 타입 매개변수에 대한 공변으로 선언하는 데 사용된다
interface Producer<out T> {
    fun produce(): T
}

/* while the generic class indeed promises to never accept a T
 * in one of its methods, the constructor is excluded from this rule
 */ 
class ReadOnlyBox<out T>(private var item: T) {
    fun getItem(): T = item
}
타입 안전성을 보장하기 위해 T는 out 위치에서만 쓸 수 있다. 즉 T는 함수에서만 반환되므로 함수에서 생성된다

< 반공변(반전된 하위 타입 관계) >

- B가 A의 하위 타입인 경우 Class<A>는 Class<B>의 하위 타입이다

Cat이 Animal의 하위 타입이기 때문에 Consumer<Animal>은 Consumer<Cat>의 하위 타입이다...(중략)...in variance 한정자는 클래스를 특정 타입 매개변수에 대해 반공변으로 선언하는 데 사용된다
interface Consumer<in T> {
    fun consume(t: T)
}

class WriteOnlyBox<in T>(private var item: T) {
    fun setItem(newItem: T) {
        item = newItem
    }
}
이 경우 T는 in 위치에서만 쓸 수 있다. 즉 T의 값이 이 클래스의 함수로 전달된다. 따라서 해당 기능에 의해 소비되고 있다. T를 함수 매개변수로 전달한다는 건 다른 동작이 필요한 함수에 의해 수정될 수 있음을 의미한다. 클래스 또는 인터페이스 하나에 대해 공변(covariant)하고 다른 타입 매개변수에 대해 반공변(contravariant)할 수 있음을 명심하라

 

https://proandroiddev.com/understanding-generics-and-variance-in-kotlin-714c14564c47

 

Understanding Generics and Variance in Kotlin

One of the biggest selling points of Object-Oriented Programming languages is inheritance.

proandroiddev.com

(중략)...먼저 코틀린 클래스 몇 개를 정의한다
abstract class Animal(val size: Int)
class Dog(val cuteness: Int): Animal(100)
class Spider(val terrorFactor: Int): Animal(1)
< 공변 >

타입을 일반 리스트로 래핑해서 좀 더 복잡한 타입을 사용한다. 한 가지 기억애햐 할 것은 이 리스트는 코틀린의 불변 리스트다. 리스트를 만든 후에는 내용을 수정할 수 없다
val dogList: List<Dog> = listOf(Dog(10), Dog(20))
val animalList: List<Animal> = dogList
variance는 Dog가 Animal의 하위 타입인 List<Dog>와 List<Animal> 간의 관계에 대해 알려준다. 코틀린에선 dogList를 AnimalList에 할당할 수 있으므로 타입 관계가 유지되고 List<Dog>가 List<Animal>의 하위 타입이 된다. 이를 공변이라고 한다

< 무공변 >

자바에선 Dog가 Animal의 하위 타입이더라도 아래를 수행할 수 없다
List<Dog> dogList= new ArrayList<>();
List<Animal> animalList = dogList; // Compiler error
이는 자바 제네릭이 구성요소 간의 타입 대 하위 타입 관계를 무시하기 때문이다. List<Dog>를 List<Animal>에 할당할 수 없거나 그 반대의 경우를 무공변이라고 한다. 여기엔 상위 타입 관계에 대한 하위 타입이 없다

< 반공변 >

compare()를 써서 Compare<T> 인터페이스를 만들 수 있고 이 메서드는 어떤 아이템이 1번째고 어떤 아이템이 2번째인지 말할 수 있다. 우리가 개를 비교할 때마다 개가 얼마나 귀여운지 본다. 아래는 개를 비교하는 코드다
val dogCompare: Compare<Dog> = object: Compare<Dog> {
  override fun compare(first: Dog, second: Dog): Int {
    return first.cuteness - second.cuteness
  }
}
이 비교 메커니즘을 Animal Comparator에 할당하려고 하면 어떻게 되는가?
val animalCompare: Compare<Animal> = dogCompare // Compiler error
이것이 작동한다면 거미를 animalCompare에 전달할 수 있지만 dogCompare는 거미가 아닌 개만 비교할 수 있기 때문에 오류가 된다. 반면 모든 동물을 비교할 수 있는 방법이 있다면 그 메커니즘은 개, 거미에 대해 작동해야 한다
val animalCompare: Compare<Animal> = object: Compare<Animal> {
  override fun compare(first: Animal, second: Animal): Int {
    return first.size - second.size
  }
}
val spiderCompare: Compare<Spider> = animalCompare // Works nicely!
Spider는 Animal의 하위 타입이지만 Compare<Animal>은 Compare<Spider>의 하위 타입인 걸 알 수 있다. 타입 관계가 반대다. 반공변이라고도 한다

코틀린에는 제네릭을 공변 또는 반공변으로 정의하는 방법이 있다

< out >

코틀린에서 불변 리스트의 정의를 보면 아래와 같다
interface List<out E> {
  fun get(index: Int): E
}
out 키워드는 List의 메서드가 E 타입만 반환할 수 있으며 E 타입을 인수로 쓸 수 없음을 나타낸다. 이 제한을 통해 리스트를 공변으로 만들 수 있다

< in >

Compare 인터페이스의 정의를 확인하면 다른 걸 볼 수 있다
interface Compare<in T> {
  fun compare(first: T, second: T): Int
}
매개변수 앞에 in 키워드가 있다. 즉 Compare의 모든 메서드는 T를 인수로 받을 수 있지만 T 타입을 반환할 수는 없다. 이건 Compare를 반공변으로 만든다. 코틀린에서 클래스 선언을 작성하는 개발자는 코드를 쓰는 개발자가 아닌 variance를 고려해야 한다. 이것이 선언 사이트 분산(declaration-site variance)이라고 하는 이유다

 

https://stackoverflow.com/questions/44298702/what-is-out-keyword-in-kotlin

 

What is out keyword in kotlin

I am not able to understand and I couldn't find the meaning of out keyword in kotlin. You can check example here: List<out T> If any one can explain the meaning of this. It would be really

stackoverflow.com

List<out T>는 자바의 List<? extends T>와 같고, List<in T>는 자바의 List<? super T>와 같다

 

반응형
Comments