코틀린 제네릭 (가변성, 스타 프로젝션, reified)
제네릭은 클래스 내부에서 사용할 자료형을 컴파일 시간에 검사하여 인스턴스를 생성할 때 확정하는 방법입니다.
제네릭을 사용하면 객체의 자료형을 컴파일할 때 체크하기 때문에 객체 자료형의 안정성을 높이고, 형 변환의 번거로움이 줄어듭니다.
제네릭을 사용하기 위해서는 앵글 브래킷(<>)
사이에 타입 매개변수를 넣어 선언합니다.
다수 조건의 타입 매개변수 제한
매개변수의 타입을 제한하기 위해서는 where
키워드를 사용하여 지정된 제한을 모두 포함하는 경우에만 허용하도록 할 수 있습니다.
다음 예제는 a와 b에 들어갈 자료형을 Number이면서 Comparable을 구현하고 있는 타입으로 제한하고 있습니다.
fun <T> getMax(a: T, b: T): T where T:Number, T:Comparable<T> {
return if (a > b) a else b
}
가변성의 3가지 유형
가변성(Variance)이란 형식 매개변수가 클래스 계층에 영향을 주는 것을 말합니다.
예를 들어 형식 A의 값이 필요한 모든 클래스에 형식 B의 값을 넣어도 아무 문제가 없다면 B는 A의 하위 형식이 됩니다.
공변성 (Covariance)
T'가 T의 하위 자료형이면, C<T'>는 C
생산자 입장의 out
성질
class TypeClass<out T>(val size: Int) // 무변성
fuc main() {
val anys: TypeClass<Any> = TypeClass<Int>(10)
val nothings: TypeClass<Nothing> = TypeClass<Int>(10) // 오류 발생
}
형식 매개변수를 갖는 프로퍼티는 var로 지정될 수 없고, val만 허용합니다.
var을 사용하려면 다음과 같이 private으로 지정해야 합니다.
class TypeClass<out T: Parent>(private var element: T)
반공변성 (Contravariance)
T'가 T의 하위 자료형이면, C
소비자 입장의 in
성질
class TypeClass<in T>(val size: Int) // 무변성
fuc main() {
val anys: TypeClass<Any> = TypeClass<Int>(10) // 오류 발생
val nothings: TypeClass<Nothing> = TypeClass<Int>(10)
}
무변성 (Invariance)
C
생산자 + 소비자
class TypeClass<T>(val size: Int) // 무변성
fuc main() {
// 다음 코드들은 자료형 불일치로 오류가 발생한다.
val anys: TypeClass<Any> = TypeClass<Int>(10)
val nothings: TypeClass<Nothing> = TypeClass<Int>(10)
}
가변성의 2가지 방법
선언 지점 변성(declaration-site variance)
클래스를 선언하면서 클래스 자체에 가변성을 지정하는 방식으로 클래스에 in/out을 지정합니다.
이는 클래스 전체적으로 공변성이 지정되기 때문에, 클래스를 사용하는 장소에서는 따로 자료형을 지정하지 않아도 되기때문에 편리합니다.
위 예제들이 선언 지점 변성
의 예입니다.
사용 지점 변성(use-site variance)
메서드 매개변수 or 제네릭 클래스를 생성할 때와 같이 사용 위치에서 가변성을 지정하는 방식입니다.
class TypeClass<T> (var parent: T)
fun <T> print(element: TypeClass<out Parent>) {
val parentObj: Parent = element.parent
println(parentObj)
}
스타 프로젝션
스타 프로젝션<*>
은 어떤 자료형이라도 들어올 수 있으나 구체적으로 자료형이 결정되고 난 후에는 그 자료형과 하위 자료형의 요소만 담을 수 있도록 제한할 수 있습니다.
in으로 정의되어 있는 타입 매개변수를 *로 받으면 in Nothing으로 간주하고,
out으로 정의되어 있는 타입 매개변수를 *로 받으면 out Any?인 것으로 간주합니다.
따라서 *을 사용할 때 그 위치에 따라 메서드 호출이 제한될 수 있습니다.
class InOut<in T, out U>(t: T, u: U) {
val prop: U = u // U는 out 위치
fun fuc(t: T) { // T는 in 위치
print(t)
}
}
fun starFuc(v: InOut<*,*>) {
v.fuc(1) // 오류! Nothing으로 인자 처리
print(v.prop)
}
reified 자료형
제네릭은 컴파일 후 런타임에는 삭제 되기때문에 제네릭 타입
그래서 c: Class<T>
처럼 제네릭 타입을 함수의 매개변수로 전달해야 타입에 접근할 수 있습니다.
하지만 reified
로 타입 매개변수 T를 지정하면 런타임에서도 접근 할수 있게 되어 c: Class<T>
와 같은 매개변수를 넘겨주지 않아도 됩니다.
하지만 reifeid 자료형은 인라인 함수에서만 사용할수 있습니다.
inline fun <reified T> fuc() {
print(T::class)
}
참고
서적 : Do it! 코틀린 프로그래밍