아규먼트로 함수에 값을 전달할 수 있는 것처럼 타입 아규먼트를 사용하면 함수에 타입을 전달할 수 있다.
타입 아규먼트를 사용하는 함수(즉, 타입 파라미터를 갖는 함수)를 제네릭 함수라고 부른다. 대표적인 예로는 stdlib의 filter()
가 있다. filter()
는 타입 파라미터 T를 갖는다.
inline fun <T> Iterable<T>.filter(
predicate: (T) -> Boolean
): List<T> {
val destination = ArrayList<T>()
for (element in this) {
if (predicate(element)) {
destination.add(element)
}
}
return destination
}
filter()의 람다 표현식 내부에서 컴파일러가 아규먼트가 컬렉션의 요소와 같은 타입이라는 걸 알 수 있으므로 잘못 처리하는 걸 막을 수 있다. IDE도 이를 기반으로 여러 유용한 제안을 해 준다.
제네릭은 기본적으로 List 또는 Set처럼 구체적인 타입으로 컬렉션을 만들 수 있게 클래스, 인터페이스에 도입된 기능이다. 컴파일 과정에서 최종적으로 이런 타입 정보는 사라지지만 개발 중에는 특정 타입을 사용하게 강제할 수 있다. 이런 타입 정보 덕분에 MutableList에 안전하게 Int를 추가할 수 있다.
또한 Set에서 요소를 꺼내면 그것이 User라는 걸 알 수 있다. 이런 기능은 정적 타입 프로그래밍 언어에선 굉장히 유용하게 활용된다.
코틀린은 강력한 제네릭 기능을 갖고 있지만 조금 복잡해서 이해하기 어렵다. 필자의 경험에 의하면 많은 코틀린 개발자가 variance 한정자를 어떤 형태로 사용하는지 잘 몰랐다.
타입 파라미터의 중요한 기능 중 하나는 구체적인 타입의 서브타입만 사용하게 타입을 제한하는 것이다. 아래 코드는 콜론 뒤에 슈퍼타입을 설정해 제한을 걸었다.
fun <T: Comparable<T>> Iterable<T>.sorted(): List<T> {
/* ... */
}
fun <T, C : MutableCollection<in T>>
Iterable<T>.toCollection(destination: C): C {
/* ... */
}
class ListAdapter<T: ItemAdapter> (/* ... */) { /*...*/ }
타입에 제한이 걸리므로 내부에서 해당 타입이 제공하는 메서드를 사용할 수 있다. 예를 들어 T를 Iterable의 서브타입으로 제한하면 T 타입을 기반으로 반복 처리가 가능하고 반복 처리 시 사용하는 객체가 Int라는 걸 알 수 있다. 또한 Comparable로 제한하면 해당 타입을 비교할 수 있다는 걸 알 수 있다. 많이 사용하는 제한으로는 Any가 있다. 이는 nullable이 아닌 타입을 나타낸다.
inline fun <T, R: Any> Iterable<T>.mapNotNull(
transform: (T) -> R?
): List<R> {
return mapNotNullTo(ArrayList<R>(), transform)
}