Primary/Kotlin

[Kotlin] 제네릭 타입 제한하기

해스끼 2022. 6. 10. 15:49

상한 (Upper bound)

다음과 같이 ``List<T>``의 확장 함수를 선언했다.

fun <T> List<T>.sum() = ...

이때 ``T``는 모든 타입이 될 수 있다. 그러나 ``List<T>.sum``은 수를 저장한 리스트에 대해서만 실행되어야 한다. 그러니까 ``T``의 타입을 제한해야 한다는 말이다.

 

Kotlin에는 모든 수(number) 타입의 부모 클래스 ``Number``가 존재한다.따라서 ``Number`` 또는 그 하위 클래스를 담은 리스트만 sum 함수를 실행할 수 있게 해야 한다.

 

다음과 같이 선언하면 된다.

fun <T : Number> List<T>.sum() = ...

이렇게 하면 ``Number`` 또는 ``Number``의 하위 타입만 ``T``가 될 수 있다. 이처럼 Kotlin에서는 제네릭 타입의 상한(upper bound)를 정할 수 있다. 클래스 상속 관계에서 ``T``가 아무리 올라가 봤자 ``Number``가 한계라는 것이다.

 

확장 함수 뿐 아니라 일반적인 함수에서도 타입의 상한을 정할 수 있다.

fun <T : Number> tenTimes(value: T): Double {
    return value.toDouble() * 10
}

아주 드문 경우로, 제한을 여러 개 걸어야 할 때도 있다. 예를 들어 다음 코드에서는 ``CharSequence``와 ``Appendable`` 인터페이스를 모두 구현한 타입만이 ``T``가 될 수 있다.

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

모든 타입?

제네릭 타입의 상한을 정하지 않으면 기본값으로 ``Any?``가 상한으로 작용한다. ``Any?`` 자체가 타입 위계질서의 맨 위에 있기 때문에 모든 타입을 ``T``로 사용할 수 있는 것이다. 그래서 기본적으로 ``T`` 타입의 변수는 ``null``이 될 수 있다.

class Processor<T> {
    fun process(value: T) {
        value?.doSomething()
    }
}

``T``에 명시적으로 ``?``를 붙이지 않았지만 ``value``는 nullable이다. ``T``를 non-nullable로 만들고 싶다면 다음과 같이 하면 된다.

class Processor<T : Any> {
    fun process(value: T) {
        value.doSomething()
    }
}

이제 ``T``는 ``null``이 될 수 없다.