제네릭 타입 파라미터

코틀린에서는 자바와는 달리, 제네릭 타입의 타입 인자를 프로그래머가 명시하거나 컴파일러가 추론할 수 있어야 합니다.

// 1. 컴파일러 타입 추론
val authors = listof("A", "B")
// 2. 변수 선언쪽에 타입 선언
val readers: MutableList<String> = mutableListOf()
// 3. 변수를 만드는 함수의 타입인자 지정
val readers = mutableListOf<String>()

1. 제네릭 함수

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

제네릭 함수의 타입 파라미터 T를 선언합니다.

 

2. 제네릭 확장 프로퍼티

val <T> List<T>.penultimate: T

3. 제네릭 클래스 선언

interface List<T> {
    operator fun get(index: Int): T
}

4. 타입 파라미터 제약

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

// 반드시 CharSequence 와 Appendable 인터페이스를 구현해야함을 표현
fun <T> ensureTrailingPeriod(seq: T) where T: CharSequence, T: Appendable

sum 함수는 숫자 타입으로만 와야 하므로 타입에 제약이 필요합니다. 자바의 <T extends Number> T sum(List<T> list)와 같이 상한 타입을 지정할 수 있습니다.

실행 시 제네릭스의 동작: 소거된 타입 파라미터와 실체화된 타입 파라미터

JVM에서는 실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않습니다. 타입 인자 정보는 런타임에 지워집니다. 이를 Erased Type이라고 합니다.

if (value is List<String>) { ... }
ERROR: Cannot check for instance of erased type // 실행시점에 타입을 알 수 없기 떄문

// 스타 프로젝션을 활용한다면 타입은 알 수 없지만 List 인 것 까지는 확인이 가능함
if (value is List<*>) { ... }

inline + reified를 활용한 런타임 타입 정보

// Error: cannot check for instance of erased type: T
// 컴파일 에러
fun <T> isA(value: Any) = value is T

// inline + reified
// 컴파일 성공
inline fun <reified T> isA(value: Any) value is T

 

reified는 인라인에서 사용할 수 있는 키워드입니다. inline을 사용하면 컴파일러는 그 함수를 호출한 식을 모두 함수 본문으로 바꿉니다. 컴파일러는 실체화된 타입 인자를 사용해 인라인 함수를 호출하는 각 부분의 정확한 타입 인자를 알 수 있습니다.

변성

List<Any> 와 List<String> 두 가지 리스트가 존재한다.

fun addAnswer(list: MutableList<Any>) { list.add(42) } 
val strings = mutableListOf("abc", "bac") 
addAnswer(strings) 
println(strings.maxBy {it.length}) // error!

List<Any> 타입의 파라미터를 받는 함수에 List<String>을 넘기면 안전할까요? 위의 예시에서 List<Any> 대신 List<String>를 넘기면 안 되는 이유는 리스트의 원소를 추가하거나 변경할 수 있기 때문입니다. 변경되지 않는다면 안전합니다. 리스트의 변경 가능성을 적절한 인터페이스로 제한한다면 이를 해결할 수 있습니다.

List 타입의 파라미터를 받는 함수에 List를 넘기면 안전할까요? 애초에 하위 타입이 성립하지 않습니다.

변성이란?

List<Any>와 List<String>과 같이 기저 타입이 같고 타입 인자가 다른 여러 타입이 서로 어떤 관계가 있는지 설명하는 개념입니다.

공변성

반공변성 무공변성
Producer Consumer MutableList
타입 인자의 하위, 타입 관계가 제네릭 타입에서도 유지됨 타입 인자의 하위 타입 관계가 제네릭 타입에서 뒤집힘 하위 타입 관계가 성립되지 않음
Producer 은 Producer 의 하위타입이다. Consumer 은 Consumer 의 하위타입이다.  
T를 아웃 위치에서만 사용할 수 있음 T를 인 위치에서만 사용할 수 있음 T를 아무 위치에서나 사용할 수 있음

공변성

out 키워드의 역할

  • 공변성: 하위 타입 관계가 유지됨 (Producer 은 Producer 의 하위타입)
  • 사용제한: T를 아웃 위치(반환)에서만 사용가능
open class Animal
class Herd<T: Animal> {
    operator fun get(i: Int): T {...}
}

fun sayHello(animals: Herd<Animal>) {
    print("hello $animals")
}

class Cat: Animal()
fun sayHelloCats(cats: Herd<Cat>) {
    sayHello(cats) // Error!
}

Cat의 부모가 Animal 이라고 해서 Herd<Cat> 의 부모가 Herd<Animal> 이라는 상하관계가 성립하는 것은 아닙니다. 그렇기 때문에 Type Mismatch 에러가 발생하는 것입니다.

이는 공변성을 선언하면 해결됩니다. 

class Herd<out T: Animal>