computer_study

[Kotlin] 09. 제네릭스 본문

스터디/코틀린 완벽 가이드

[Kotlin] 09. 제네릭스

knowable 2022. 6. 8. 23:23

알지 못하는 타입의 데이터를 조작하는 코드를 작성할 수 있게 해준다.

9.1 타입 파라미터

9.1.1 제네릭 선언

class TreeNode<T>(val data: T){
    private val _children = arrayListOf<TreeNode<T>>()
    var parent: TreeNode<T>? = null
    private set
    ...
    fun addChild(data: T) = 
    ...
}

fun main(){
    val root = TreeNode("Hello").apply{
        addChild("World")
        addChild("!!!")
    }
    ...
}
  • 타입 파라미터는 클래스 이름 바로 뒤에
  • 타입 파라미터는 관습적으로 T, U, V등의 짧은 대문자를 사용한다.
  • java처럼 raw타입을 사용할 수 없고, TreeNode<String> 처럼 구체적인 타입을 지정하거나 TreeNode<U>처럼 타입 인자로 받은 타입을 지정해주어야 한다.
  • 제네릭 클래스에서와 달리 프로퍼티나 함수를 제네릭으로 선언할 때는 타입 파라미터를 fun이나 val/var 바로 뒤에 위치 시킨다

9.1.2 바운드와 제약

데이터 타입에 정보가 필요한 경우에 상위 바운드에 선언하여 제약을 할 수 있다. 

fun <T : Number>TreeNode<T>.average(): Double{
    var count = 0
    var sum = 0.0
    ...
    return sum/count
}

///////
val StringTree = TreeNode("Hello").apply{
    addChildren("World", "!!!")
}
println(stringTree.average()) // 이렇게 호출한다면 제약이 있기에 컴파일 오류가 발생한다
  • 자바는 T extends Number이지만 코틀린은 T: Number이다.
  • 타입 파라미터를 널이 아닌 타입으로 제한하는 경우에도 사용
fun <T: Any> notNullTreeOf(data: T) = TreeNode(data)

9.1.3 타입 소거와 구체화

JVM은 기존 자바와 새 버전의 자바 호환을 위해, 새 타입 인자에 대한 정보가 코드에서 지워지고(타입 소거) 다른 타입으로 합쳐진다.

자바에선 해결을 위해 케스트나 리플렉션을 사용하지만 코틀린에선 구체화를 사용한다.

(인라인한 함수에 대해서만 구체화한 타입 파라미터 사용 가능)

 

fun <T>TreeNode<T>.cancellableWalkDepthFirst(
    onEach: (T) -> Boolean
): Boolean{
    ...
}
inline fun <reified T> TreeNode<*>.isInstanceOf() =
    cancellableWalkDepthFirst{ it is T }
    
////// 호출 시
fun main(){
    val tree = TreeNode<Any>("abc").addChild("def").addChild(123)
    println(tree.isInstanceOf<String>())
}
//// 아래와 같아진다
fun main(){
    val tree = TreeNode<Any>("abc").addChild("def").addChild(123)
    println(tree.cancellableWalkDepthFirst { it is String })
}

9.2 변성

9.2.1 변성: 생산자와 소비자 구분

  • 생산자
    • T타입의 값을 반환하는 연산만 제공하고 T 타입의 값을 입력으로 받는 연산은 제공하지 않는 제네릭 타입
    • X가 생산자 역할을 한다면, T를 공변적으로 선언할 수 있고, A가 B의 하위 타입이면 X<A>가 X<B>의 하위 타입이 된다.
  • 소비자
    • T타입의 값을 입력으로 받기만 하고 결코 T타입의 값을 반환하지 않는 제네릭 타입
    • X가 소비자 역할을 한다면 T를 반공변적으로 선언할 수 있고, B가 A의 하위 타입이면 X<A>가 X<B>의 하위 타입이 된다.

9.2.2 선언 지점 변성

타입 파라미터의 변성을 선언 자체에 지정하는 방법

interface List<out T>{ // T에 대해 공변적으로 만드는 방법
    val size: Int
    fun get(index: Int): T
}
/////
// List<Int>가 List<Number의 하위타입
  • 어떤 타입 파라미터가 항상 out위치에서 쓰이는 경우에만 파라미터를 공변적으로 선언할 수 있다
class Writer<in T>{
    fun write(value: T){
        println(value)
    }
}
  • 반공변인 타입 파라미터 앞에 in 키워드를 붙일 수 있다.
  • 제네릭 타입이 소비자 역할을 할 때 타입 파라미터를 in으로 표시할 수 있다.

9.2.3 프로젝션을 사용한 사용 지점 변성

프로젝션 : 제네릭 타입을 사용하는 위치에서 특정 타입 인자 앞에 in/out을 붙이는 방법.

코틀린의 프로젝션은 자바의 extends/super 와일드카드와 같은 역할을 한다

  • TreeNode<out Number>
    • 자바에선 TreeNode<? extends Number> 
  • TreeNode<in Number>
    • 자바에선 TreeNode<? super Number>

9.2.4 스타 프로젝션

*로 표기된다.

타입 인자가 타입 파라미터의 바운드 안에서 아무 타입이나 될 수 있다는 사실을 표현

 

코틀린의 스타 프로젝션은 자바의 ? 와일드 카드에 대응

  • TreeNode<*>
    • 자바에선 TreeNode<?>

9.3 타입 별명

제네릭 타입을 다룰 때 도움이 되는 언어 기능

긴 이름을 짧게 부를 수 있도록 해준다.

typealias IntPredicate = (Int) -> Boolean // typealias [별명] = [실제 타입]
typealias IntMap = HashMap<Int, Int>

fun readFirst(filter: IntPredicate) = 
    generateSequence{ readLine()?.toIntOrNull() }.firstOrNull(filter)

fun main(){
    val map = IntMap().also{
        it[1] = 2
        it[2] = 3
    }
}

// 파라미터 포함도 가능
typealias ThisPredicate<T> = T.() -> Boolean
typealias MultiMap<K, V> = Map<K, Collection<V>>

 

 

 

 

Comments