지난 글 제네릭에 이어서 변성에 대해 살펴보겠습니다. 지난 글에서 설명드린 것처럼 제네릭의 다형성은 제한을 갖습니다. 대입된 타입이 상속관계라 할지라도 다형성이 성립되지 않았죠. 변성을 사용하면 이러한 제한을 해제할 수 있습니다.
변성은 어려운 개념입니다. 아직 이해하기 어렵다면 건너뛰셔도 좋습니다!
변성 Variance
변성이란 형식 매개변수가 클래스 계층에 영향을 주는 것을 말합니다.
어떻게 영향을 미치는지에 따라 공변성, 반공변성, 무변성으로 분류됩니다.
다음 표는 T’가 T의 하위 자료형일 경우에 각 변성에 따라 어떻게 달라지는 지를 나타냅니다.
공변성(Covariance) | C<T’>는 C<T>의 하위 자료형 (생산자 입장, get, out 키워드 사용) |
반공변성(Contravariance) | C<T>는 C<T’>의 하위 자료형 (소비자 입장, set, in 키워드 사용) |
무변성(Invariance) | C<T>와 C<T’>는 관계 없음 (기본적인 generic, get, set) |
변성 지정 방법
- 선언 지점 변성 : 클래스를 선언하면서 변성을 지정하는 방식
- 사용 지점 변성 : 사용 위치에서 변성을 지정하는 방식
공변성 : out
1. 선언 지점 변성
- T는 반환 자료형에만 사용할 수 있습니다.
- 형식 매개변수를 갖는 프로퍼티는 val이나 private var만 허용됩니다.
class Box<out T> (val num: Int)
fun main() {
val value: Box<Any> = Box<Int>(10) // Int가 Any의 하위 자료형이기 때문에 객체 생성 가능
val value: Box<Int> = Box<Any>(10) // 오류
}
2. 사용 지점 변성
- 값을 얻을 때 (get) 사용합니다.
- 클래스에 직접 out을 선언하지 않고 사용 위치에서 입력합니다.
open class Fruit(val price: Int)
class Box<T> (var item: T)
fun <T> printObj(box: Box<out Fruit>) {
val obj: Fruit = box.item // item의 값을 얻음(get)
box.item = Fruit() // item의 값을 설정(set) // 오류
}
반공변성 : in
1. 선언 지점 변성
- 매개변수에만 사용할 수 있습니다.
class Box<in T> (val num: Int)
fun main() {
val value: Box<Any> = Box<Int>(10) // 오류
val value: Box<Int> = Box<Any>(10) // Int가 Any의 하위 자료형이기 때문에 객체 생성 가능
}
2. 사용 지점 변성
- 값을 설정할 때 (set) 사용합니다.
- 클래스에 직접 in을 선언하지 않고 사용 위치에서 입력합니다.
open class Fruit(val price: Int)
class Box<T> (var item: T)
fun <T> printObj(box: Box<in Fruit>) {
val obj: Fruit = box.item // item의 값을 얻음(get) //오류
box.item = Fruit() // item의 값을 설정(set)
}
무변성
class Box<T> (val num: Int)
fun main() {
val value: Box<Any> = Box<Int>(10) // 오류
val value: Box<Any> = Box<Int>(10) // 오류
}
자료형 프로젝션
위에서 본 것처럼 get을 위해서는 out으로 지정하고, set을 위해서는 in으로 지정해야 합니다.
이처럼 사용하고자 하는 요소의 자료형으로 in/out을 지정해 get, set의 사용을 제한하는 것을 자료형 프로젝션이라 합니다.
<Any?> : 모든 자료형의 요소를 담을 수 있습니다.
<*> : 어떤 자료형이라도 들어올 수 있으나 구체적으로 자료형이 결정되고 난 후에는 그 자료형과 하위 자료형의 요소만 담을 수 있습니다.
in으로 정의되어 있는 형식 매개변수를 *로 받으면 in Nothing인 것으로 간주합니다.
out으로 정의되어 있는 형식 매개변수를 *로 받으면 out Any? 인 것으로 간주합니다.
❓ Nothing 클래스란?
- 코틀린의 최하위 자료형으로 이름처럼 아무것도 가지지 않는 클래스를 의미합니다. 주로 반환 자료형으로 사용됩니다. 반환하는 값이 아무 의미가 없을 때 사용합니다.
reified 자료형
함수 내부에서 자료형 T에 접근하기 위해서는 inline과 reified 키워드를 사용합니다.
원래 자료형 T는 컴파일 시에만 접근이 가능하지만 reified 자료형을 사용하면 실행 시간에도 접근이 가능해집니다.
inline fun <reified T> reifiedFun() {
println(T::class)
println(T::class.java)
}
❓ T::class, T::class.java는 무엇을 의미하나요?
- T::class는 코틀린의 KClass, T::class.java는 자바의 Class를 의미합니다. KClass, Class 클래스는 원본 클래스에 대한 메타 데이터를 가지고 있습니다. 출력 시 class kotlin.클래스명, class java.클래스명 등으로 출력됩니다.
제네릭과 이어지는 변성에 대해 살펴봤습니다. 어려운 내용이라 시작부터 주춤했을 분들이 많으셨을 수도 있겠습니다. 어려울 땐 쓱 훑어보고 넘어가시면 됩니다. 반복적으로 읽었을 때 언젠가는 자기 것이 되겠죠! 다음 글에서는 배열을 공부합시다.
'개발언어 > Kotlin : 코틀린' 카테고리의 다른 글
코틀린 익히기 11 - 컬렉션 프레임워크 (1) | 2023.10.04 |
---|---|
코틀린 익히기 10 - 배열 (0) | 2023.10.01 |
코틀린 익히기 8 - 제네릭 (0) | 2023.09.28 |
코틀린 익히기 7 - 다양한 클래스와 인터페이스 (0) | 2023.09.27 |
코틀린 익히기 6 - 프로퍼티와 object (0) | 2023.09.25 |