본문으로 바로가기

[Kotlin] 코틀린 Generic #1

category 개발이야기/Kotlin 2018. 5. 12. 19:58
반응형

이 글은 Kotlin In Action을 참고 하였습니다.

더욱 자세한 설명이나 예제는 직접 책을 구매하여 확인 하시기 바랍니다


9.1 Generic type parameter

코틀린도 자바와 같이 Generic을 지원합니다.

일상적으로 사용하기에는 자바와 다르지 않으나, 코틀린 좀더 많은 Generic 기능을 지원합니다.

자바는 1.5부터 제네릭 개념이 들어가면서 하위 호환성을 위해 타입을 정의하지 않고도 사용할 수 있으나, 코틀린은 반드시 타입을 정의하고 써야 합니다.

예를 틀면 자바는 List로 타입 선언이 가능하지만, 코틀린은 List<String> 처럼 반드시 타입을 넣어야 합니다.


9.1.1 제네릭 함수와 Property

fun <T> List<T>.slice(indices: IntRange): List<T>

제너릭 함수는 위와 같이 정의합니다.

아직은 자바와 별다를게 없습니다.

호출할때 type 인자를 명시적으로 넣어도 되지만 넣지 않더라도 컴파일러가 알아서 타입을 추론합니다.

fun main(args: Array) {
    val letters = ('a'..'z').toList()
    println(letters.slice<char>(0..2))
    println(letters.slice(10..13))
}


Extension property만 제너릭 하게 만들 수 있습니다.

일반 property에 generic을 사용하면 컴파일 오류가 납니다.


9.1.2 제네릭 클래스

자바와 같은 방법으로<>형태로 선언하면 됩니다.

9.1.3 Type parameter limitation

자바에는 extendssuper를 사용하여 사용한 타입을 제한할 수 있습니다.
코틀린에서는 : 를 사용하여 상한 type (uppper bound)를 설정 할 수 있습니다.

// 자바
<T extends Number> T sum(List<T> list)

//코틀린
fun <T: Number> List<T>.sum():T

코틀린에서 타입 상한을 설정하면 당연한 예기지만 상한 타입으로 취급할 수 있습니다.

fun  oneHalf(value: T): Double {
    return value.toDouble() / 2.0 // toDouble()는 Number의 함수다.
}

fun main(args: Array) {
    println(oneHalf(3))
}



fun <T: Comparable<T>> max(first: T, second: T): T {
    return if (first > second) first else second
}

fun main(args: Array) {
    println(max("kotlin", "java"))
}

위 예제에서 T는 Comparable<T>를 상한으로 갖습니다.

즉. max()를 사용할때의 인자는 Comparable을 구현하고 있어야 한다는 의미가 됩니다.


String은 Comparable을 구현하고 있으므로 해당 코드는 정상적으로 동작합니다.

물론 int와 String을 넣으면 컴파일 에러가 나겠죠?


여담이지만 first > second 에서 > 연산자는 코틀린 convention에 따라 first.compareTo(second)로 변경됩니다.

(기억이 안난다면 http://tourspace.tistory.com/118?category=797357 를 다시한번 읽어보셔도 됩니다.


import java.time.Period

fun <T> ensureTrailingPeriod(seq: T)
        where T : CharSequence, T : Appendable {
    if (!seq.endsWith('.')) {
        seq.append('.')
    }
}

fun main(args: Array<String>) {
    val helloWorld = StringBuilder("Hello World")
    ensureTrailingPeriod(helloWorld)
    println(helloWorld)
}
아주 드물지만 두개의 제약을 걸어야 하는 경우 where 연산자를 씁니다.

이때의 함수타입은 나열된 타입들을 전부 만족해야 합니다.


9.1.4 Non-Null Type parameter 설정

코틀린에서 타입의 기본은 Non null 입니다.
다만 제너릭인 경우에만 기본값이 Nullable 입니다. (한참 초반에 앞쪽 포스팅에서 언급했습니다.)
class Friend <T> {
    fun getUniqueId(value: T) {
        value?.hashCode()   //null 처리가 필요하다
    }
}

fun main(string: Array) {
    val friend = Friend()
    friend.getUniqueId(null)  //가능
}


따라서 NonNull인 타입으로 제한하려면 명시적으로 <T : Any> 로 선언해야 합니다.

<T> 만 선언한다면 <T: Any?>와 같습니다.


9.2 Generic의 run time 동작

자바에서는 실행시점에 instance의 타입인자를 확인할 수 없습니다. 이는 JVM이 type erase를 하기 때문입니다.
다만 코틀린에서는 inline을 통해서 이를 피할 수 있습니다.
이유가 무엇이면 무슨 이점이 있을까요?

9.2.1 제네릭의 run time

클래스가 생성되어 instance가 되면 더이상 인자 정보를 가지고 있지 않습니다. 예를들어 List<String>을 객체로 만들었다면, 이는 List 타입만 알 뿐 내부에 저장된 원소 type의 정보는 알 수 없습니다.
이미 컴파일러가 type에 맞는 원소만 담고 있을거라는 가정하여 run time에서는 이미 맞는 type만 들어있다고 생각합니다.
또한 원소의 타입까지는 저장하지 않으면서 메모리적인 이득도 얻습니다.

if (value is List<String>) 은 코드상의 value가 List<String>일때만 문제가 없고, value가 List<Int> 라면 컴파일이 실패 합니다.
따라서 원소의 타입과 상관없이 List인지 아닌지(set 이나 Map)을 구분하려면 * 연산자를 쓰면 됩니다.(자바의 ?와 비슷합니다.)
fun printSum(c: Collection<*>) {
    val intList = c as? List  // 여기서 warning 발생
            ?: throw IllegalArgumentException("List is expected")
    println(intList.sum())
}

fun main(args: Array) {
    printSum(listOf(1, 2, 3))
    val list = listOf(1,2,3)
    if (list is List<Int>) {
        println("ok")
    }
}

만약 인자가 두개 이상이라면 인자 개수만큼 *을 표시해야 합니다


9.2.2 reified로 타입 실체화

인라인 함수를 사용하면 type 정보를 남길 수 있습니다.
따라서 아래 예제에서는 컴파일 오류가 발생하지 않습니다.
inline fun <reified T> isA(value: Any) = value is T //컴파일 가능!!

fun main(args: Array) {
    println(isA<String>("abc"))
    println(isA<Int>(123))
}


list 함수중에 filterIsInstance를 사용하면 특정 타입의 원소만 분리해 낼 수 있습니다.

fun main(args: Array) {
    val items = listOf("one", 2, "three")
    println(items.filterIsInstance<String>())
}

이 함수는 내부적으로 for문을 돌면서 if (element is T)를 체크합니다.

단 자바에서는 reified가 붙은 함수는 호출할 수 없습니다.

또한 inline 함수는 람다가 들어간 경우에만 최적화 될수 있다고 얘기했지만, 이 경우 타입 실제화를 위해서 inline을 함수를 써야만 합니다.

만약 함수 내용이 길다면, 실제화가 필요없는 부분은 따로 함수로 뽑아내는게 효율적입니다.


9.2.3 실체화한 타입으로 클래스 참조

자바를 쓰다보면 class를 인자로 넣어야 하는 경우가 있습니다.
안드로이드의 경우 service를 start할때나, activity를 start할때, Intent에 class명을 직접 입력하여 사용합니다.

val intent = Intent(this, MainActivity::class.java)
startActivity(intent)


이는 MainActivity::class.java는 MainActivity.class의 코틀린 표현입니다.

하지만 reified를 이용하면 다르게 표현할 수 있습니다.

inline fun <reified T : Activity> Context.startActivity() {
    val intent = Intent(this, T::class.java)
    startActivity(intent)
}
startActivity<MainActivity>()

9.2.4 Reified type parameter limitation

Refied에 대한 개념을 얘기했으나, 사실 어디다가 써야할지 예제 이외에는 감이 오질 않습니다.
정리해 보면 아래와 같습니다.
  • 사용가능 case
    • type 검사와 캐스팅 (is, !is, as, as?)
    • 추후 언급되는 코틀린 리플렉션API(::class)
    • 코틀린타입에 대응하는 java.lang.Class 얻기 (::class.java)
    • 다른 함수를 호출할 대 타입 인자로 사용
  • 용 불가 case
    • Type 파라미터 클래스의 인스턴스 생성
    • Type 파라미터의 companion object method 호출
    • reified 되지 않는 type을 받아 reified type을 받는 함수에 넘기기
    • 클래스, property, inline 함수가 아닌 함수의 타입 파라미터를 reified로 지정하기

앞서 inline은 내용이 짧고, 타입을 람다로 받는 경우에 유용하다고 했습니다.
단, 이런 경우가 아니더라도 reified 가 필요하다면 inline 함수를 만들어 사용해야 합니다.
이때 람다의 본문을 옴길 수 없는 상황이거나, 성능상 람다를 inline을 하고 싶지 않다면 noinline으로 설정해야 합니다.


반응형