변성
- 변성이란?
List<String>, List<Any>
와 같이 기저 타입이 같고 타입 인자가 다른 여러 타입이 서로 어떤 관계가 있는지 설명하는 개념
- 변성? 이런 관계가 왜 중요하지?
- 코틀린에서 제네릭 클래스나 함수를 정의하는 경우
- 타입 안정성을 보장하는 API를 만드는 경우
- 꼭 잘 알고 사용해야 한다.
- 변성이 있는 이유
List<Any>
타입의 파라미터를 받는 함수에List<String>
을 넘기면 안전할까?1 2 3 4 5 6 7
fun printContentList(list: List<Any>) { println(list.joinToString()) } printContentList(listOf("abc", "bac")) // abc, bac
- 음 컴파일, 동작에도 문제없고 딱 봐도 문제가 전혀 없어보인다.
이제 리스트를 변경하는 다른 함수를 보자
1 2 3 4 5 6 7
fun addAnswer(list: MutableList<Any>) { list.add(42) } val stringList = mutableListOf("abc", "bac") addAnswer(stringList) // 이미 여기서 부터 인텔리제이가 오류를 내뱉어 준다. println(stringList)
addAnswer(stringList)
부분 에서 부터 애초에 컴파일이 안된다.- 위 예제는
MutableList<Any>
가 필요한 곳에MutableList<String>
을 넘기면 안된다는 사실을 보여준다
- 이제
List<Any> 타입의 파라미터를 받는 함수에 List<String>을 넘기면 안전할까?
라는 질문에 답할 수 있다.- 어떤 함수가 리스트의 원소를 추가하거나 변경한다면 타입 불일치가 생길 수 있어서 → 불가
- 원소 추가나 변경이 없는 경우에는
List<Any>
타입 대신List<String
>을 대신 넘겨도 안전하다.
- List 클래스와 MutableList의 변성이 왜 다를까?
- List 뿐 아니라 모든 제네릭 클래스에 대해 같은 질문을 해보고 일반화 해보자!
## 클래스, 타입, 하위 타입
List 뿐 아니라 모든 제네릭 클래스에 대해 같은 질문을 해보고 일반화 해보자!
라는 질문에 답하기 전에 먼저 타입, 하위타입 이라는 개념에 대해 먼저 알아보자- 간단히 설명하면
- String 은 클래스지만 String? 은 타입이다.
- List는 클래스지만 List
, List , List<List >은 타입이다.
- 타입 사이의 관계를 논하기 위해 하위 타입 이라는 개념을 알아야 한다.
- 하위타입
- A의 값이 필요한 모든 곳에 어떤 타입 B의 값을 넣어도 아무 문제가 없다면 → 타입B는 타입A의 하위 타입이다.
- ex) Int는 Number의 하위타입 / 모든 타입은 자신의 하위타입
- 상위타입
- 하위 타입의 반대
- 그렇다면 하위 클래스 란?
- 간단한 경우 보통은 하위 타입은 하위 클래스와 동일하다.
- ex) Int클래스는 Number클래스의 하위 클래스, String은 CharSequence의 하위 클래스이자 하위 타입
- 하지만! 널이 될 수 있는 타입은 ⇒ 하위 타입과 하위 클래스가 같지 않다는걸 보여주는 예이다.
- ex) A는 A?의 하위타입이지만 A?는 A의 하위타입이 아니다. 하지만 두타입 모두 같은 클래스에 해당한다.
- 간단한 경우 보통은 하위 타입은 하위 클래스와 동일하다.
- 다시 처음으로 돌아와서
List<Any> 타입의 파라미터를 받는 함수에 List<String>을 넘기면 안전할까?
라는 질문을 하위 타입 관계를 써서 다시 쓰면List<String>
는List<Any>
의 하위 타입인가? ⇒ 맞다MutableList<String>
는MutableList<Any>
의 하위타입인가? ⇒ 아니다
- 무공변
- 제네릭 타입을 인스턴화할 때 타입 인자로 서로 다른 타입이 들어가면 인스턴스 타입 사이의 하위 타입 관계가 성립하지 않으면 그 제네릭 타입을 무공변 이라고 한다.
MutableList
로 예를 들면 A와 B가 서로 다르기만 하면MutableList<A>
는 항상MutableList<B>
의 하위 타입이 아니다. (무공변)
- 제네릭 타입을 인스턴화할 때 타입 인자로 서로 다른 타입이 들어가면 인스턴스 타입 사이의 하위 타입 관계가 성립하지 않으면 그 제네릭 타입을 무공변 이라고 한다.
- 공변 : 하위 타입 관계가 유지된다.
- List로 예를 들면 A가 B의 하위타입이면
List<A>
는List<B>
의 하위타입이다. - → List 클래스는 공변적이다.
- → 하위 타입 관계가 유지된다.
- 특정 클래스나 인터페이스가 공변적임을 선언하는 방법은?
타입 파라밑터 이름 앞에
out
을 넣으면 된다.1 2 3
interface List<out T> { fun test(): T }
- 이렇게 타입 파라미터를 공변적으로 만들면 함수 정의에 사용한 파라미터 타입과 타입인자의 타입이 정확히 일치하지 않더라도 그 클래스의 인스턴스를 함수 인자나 반환값으로 사용할 수 있다.
- List로 예를 들면 A가 B의 하위타입이면
공변
고양이 예시
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
open class Animal { fun feed() { println("feed....") } } class AnimalFamily<out T : Animal> { val size: Int get() { TODO() } operator fun get(i: Int): T { TODO() } } fun feedAll(animalFamily: AnimalFamily<Animal>) { for (i in 0 until animalFamily.size) { animalFamily[i].feed() } } class Cat : Animal() { fun cleanTrash() { TODO() } } fun takeCareOfCats(catList: AnimalFamily<Cat>) { for (i in 0 until catList.size) { catList[i].cleanTrash() } feedAll(catList) }
- 문제점 : 고양이 가족은 ⇒ 동물 가족의 하위 클래스가 아니다.
- 해결방법?
AnimalFamily
클래스는 동물을 추가하거나 가족안의 동물을 다른 종류의 동물로 바꿀 수 없다.따라서
AnimalFamily
클래스를 공변적 클래스로 만들면 된다.1
class AnimalFamily<out T : Animal> {
- 타입 파라미터를 공변적으로 만들면 → 클래스 내부에서 그 파라미터를 사용하는 방법이 제한된다.
- 이는 클래스가 T 타입의 값을 생산할 수는 있지만 T 타입의 값을 소비할 수는 없다는 뜻
- T가 함수의 반환 타입에 쓰인다면 T는 out 위치에 있어야 함 - 생산 (out)
T가 함수의 파라미터 타입에 쓰인다면 T는 in 위치에 있어야 함 - 소비 (in)
1 2
fun(t: T): T //in // out
- 즉, out 키워드는 T로 인해 생기는 하위 타입 관계의 타입 안정성을 보장한다.
ex)
1 2 3 4 5 6 7 8 9 10
class AnimalFamily<out T : Animal> { val size: Int get() { TODO() } operator fun get(i: Int): T { TODO() } }
AnimalFamily
에서 타입 파라미터 T를 사용하는 장소는 오직 get 메소드의 반환 타입뿐!- 따라서 이 클래스를 공변적(하위 타입 관계가 유지되는 상태로)으로 선언해도 안전하다.
- 마지막 정리 T에 붙은 out 키워드는 2가지 역할을 한다.
- 공변성
- 하위 타입 관계가 유지 된다. (A가 B의 하위타입이면
List<A>
는List<B>
의 하위타입이다.)
- 하위 타입 관계가 유지 된다. (A가 B의 하위타입이면
- 사용 제한
- T를 아웃 위치에서만 사용할 수 있다.
- (생성자 파라미터는 in, out 어느쪽도 아님에 유의)
- 공변성
- 이렇게 변성은 코드에서 위함할 여지가 있는 메소드를 호출할 수 없게 만드는 역할을 한다.
반공변
- 공변의 반대
- in 키워드
- T값을 소비하기만 한다. ⇒ 함수의 파라미터로만 받을 수 있다.
- 타입 B가 타입 A의 하위 타입인 경우
Class<A>
가Class<B>
의 하위 타입인 관계가 성립하면 제네릭 클래스Class<T>
는 타입인자 T에 대해서 반공변이다.- A와 B의 위치가 뒤바뀐다 → 하위 타입 관계가 뒤집힌다고 표현함
ex)
1 2 3 4 5
listOf("AAA", "BBB").sortedWith( Comparator<Any> { e1, e2 -> e1.hashCode() - e2.hashCode() } )
1 2 3
listOf("AAA", "BBB").sortedWith { e1, e2 -> e1.hashCode() - e2.hashCode() }
- 위 두개는 같은 코드
Comparator는
1 2 3
interface Comparator<in T> { fun compare(e1: T, e2: T): Int { ... } }
- 대충 이렇게 구현 되어있는데
- 어차피 T라는 타입을 함수의 인자로만 사용하기 때문에
- 위 예제에서 sortedWith가
Comparator<String>
을 받도록 되어있지만 더 일반적인 타입을 비교할 수 있는Comparator<Any>
를 넘겨도 안전하다. - 즉, String은 Any의 하위 타입 이지만 논리적으로
Comparator<Any>
가Comparator<String>
의 하위 타입이 되었다.
정리
- Cat은 Animal의 하위 타입
Class<Cat>
은Class<Animal>
의 하위 타입 : 공변성Class<Animal>
은Class<Cat>
의 하위 타입 : 반공변성
무공변성
Class<T>
- T를 어느 위치에서나 사용가능
공변과 반공변의 공존
- 클래스나 인터페이스가 어떤 타입 파라미터에 대해서는 공변적이면서 다른 타입 파라미터에 대해서는 반공변적일 수 있다.
- Function 인터페이스가 고전적인 예
ex) 파라미터가 하나뿐인 Function 인터페이스인 Function1
1 2 3
interface Function1<in P, out R> { operator fun invoke(p: P): R }
위 코드는 사실상 코틀린에서는 아래와 같다
1 2
(P)-> R // Funcation1<P, R>
선언 지점 변성과 사용자 지점 변성
- 선언 지점 변성
- 지금까지 봤던 클래스를 선언하면서 변성을 지정하는 방식
- 해당 클래스를 사용하는 모든 장소에 변성 지정자가 영향을 끼치므로 편리하다
- 사용자 지점 변성
- 타입 파라미터가 있는 타입을 사용할 때마다. 해당 타입 파라미터를 하위 타입이나 상위 타입 중 어떤 타입으로 대치할지 명시하는 방법
- 코틀린도 지원한다.
- 코틀린 → 자바
MutableList<out T>
⇒MutableList<? extends T>
MutableList<in T>
⇒MutableList<? super T>
- 코틀린 → 자바
참고 : 코틀린 인 액션 이미지 참고 : https://www.slipp.net/wiki/pages/viewpage.action?pageId=30769686
Comments powered by Disqus.