제네릭 타입 파라미터
코틀린에서는 자바와는 달리, 제네릭 타입의 타입 인자를 프로그래머가 명시하거나 컴파일러가 추론할 수 있어야 합니다.
// 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>
'🔍Kotlin' 카테고리의 다른 글
[코틀린 인 액션] backing 필드, backing 프로퍼티 그리고 위임 프로퍼티 (0) | 2024.03.03 |
---|---|
[코틀린인액션] 고차함수: 파라미터와 반환 값으로 람다 사용 (3) | 2024.01.07 |