카샤의 만개시기

코틀린 제네릭 (가변성, 스타 프로젝션, reified) 본문

Kotlin

코틀린 제네릭 (가변성, 스타 프로젝션, reified)

SKaSha 2019. 11. 30. 07:00

제네릭은 클래스 내부에서 사용할 자료형을 컴파일 시간에 검사하여 인스턴스를 생성할 때 확정하는 방법입니다.
제네릭을 사용하면 객체의 자료형을 컴파일할 때 체크하기 때문에 객체 자료형의 안정성을 높이고, 형 변환의 번거로움이 줄어듭니다.
제네릭을 사용하기 위해서는 앵글 브래킷(<>) 사이에 타입 매개변수를 넣어 선언합니다.

다수 조건의 타입 매개변수 제한

매개변수의 타입을 제한하기 위해서는 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는 C<T'>의 하위 자료형이다.
소비자 입장의 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와 C<T'>는 아무 관계가 없다.
생산자 + 소비자

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! 코틀린 프로그래밍

'Kotlin' 카테고리의 다른 글

Kotlin takeIf와 takeUnless함수  (0) 2019.12.08
Kotlin Scope Functions (apply, with, let, also, run, use)  (0) 2019.12.08
코틀린 object  (0) 2019.08.02
코틀린 한정클래스 (Sealed)  (0) 2019.08.02
코틀린 Unit vs Nothing  (0) 2019.08.02
Comments