9장에서 다루는 내용
- 제너릭 함수와 클래스를 정의하는 방법
- 타입 소거와 실체화한 타입 파라미터
- 선언 지점과 사용 지점 변성
기본적인 제너릭 개념과 타입끼리도 상하 관계가 있고, 함수 내에서만 자식용으로 쓰인다던지 역전된다던지 하는 아주 개판이다.
클래스를 많이 다뤄야지 이해할 수 있고 의미가 있는 장인 것 같다. 나중에 다시 와서 읽으라고
9.1 제너릭 타입 파라미터
는 자바와 비슷한 개념임.
val readers1: MutableList<String> = mutableListOf()
val readers2 = mutableListOf<String>()
위의 두 코드는 같다. 안에 element를 쥐어주면 컴파일러가 알아서 타입을 추론하지만, 빈 리스트일 경우는 추론할 만한게 없다보니 직접 타입을 지정해줘야 한다.
제너릭 타입을 무조건 하기만 하면 한도 끝도 없기때문에 '숫자 타입이면 다됨', '문자 타입이면 다됨' 같은 타입 파라미터 제약을 걸어줄 수 있다.
fun <T : Number> List<T>.sum(): T
fun <T: Comparable<T>> max(first: T, second: T): T {
return if (first > second) first else second
}
사용할 일이 드물긴 하지만, 한 번에 여러 제약도 가능
fun <T> ensureTrailingPeriod(seq: T)
where T : CharSequence, T : Appendable {
if (!seq.endsWith('.')) {
seq.append('.')
}
}
val helloWorld = StringBuilder("Hello World")
ensureTrailingPeriod(helloWorld)
println(helloWorld)
// Hello World.
Any? 와 Any는 달라서, null이 될 수 없는 타입으로 한정하고 싶으면 Any로 제약걸면 된다.
class Processor<T : Any> {
fun process(value: T) {
value.hashCode()
}
}
9.2 실행 시 제네릭스의 동작: 소거된 타입 파라미터와 실체화된 타입 파라미터
여기서부턴 책에서 정말 쉽고 간단하게 설명하네 마네 하는데 진짜 너무너무너무 어렵다.
많은 시간 후에 다시 와서 봐야할 것 같다.
소거된 타입 파라미터라는게 무슨 말이냐면, 컬렉션 안에 들어있는 엘리먼트들의 타입을 List<String>, List<Int>처럼 지정해줬어도 정작 런타임에는 그냥 List만 남는다. 즉, 실행 시점에는 어떤 타입의 원소를 저장하는지 알 수 없다.

그래서 원소를 추가 삭제할 수 있는 Mutable을 사용할 땐 조심해야 한다.
런타임 시에는 잘 모르기 때문에 경고문만 띄워주고 계속 진행된다.
fun printSum(c: Collection<*>) {
val intList = c as? List<Int> // 여기서 Unchecked cast: List<*> to List<Int> 경고 발생
?: throw IllegalArgumentException("List is expected")
println(intList.sum())
}
printSum(listOf(1, 2, 3)) // 6
//printSum(setOf(1, 2, 3)) // java.lang.IllegalArgumentException: List is expected
//printSum(listOf("a", "b", "c")) // java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
무슨 타입이 들어갈 지 모른다는 표시인 스타 프로젝션(star projection) 대신 타입을 넣어주면 코틀린 컴파일러는 컴파일 시점에 타입 정보가 주어진 경우에는 is 검사를 수행하게 허용할 수 있을 정도로 똑똑하다.
fun printSum(c: Collection<Int>) {
if (c is List<Int>) {
println(c.sum())
}
}
실행 시점에도 알 수 있는 방법이 inline을 사용하는 건데, 함수 본문 통채를 치환해주기 때문이다.
fun <T> isA(value: Any) = value is T
// Error: Cannot check for instance of erased type: T
inline fun <reified T> isA(value: Any) = value is T
println(isA<String>("abc")) // true
println(isA<String>(123)) // false
val items = listOf("one", 2, "three")
println(items.filterIsInstance<String>())
reified 키워드는 이 타입 파라미터가 실행 시점에 지워지지 않음을 표시한다. 즉, 실행 시에도 사라지지 않기 때문에 함수 내부에서도 갖다 쓸 수가 있다고
inline fun <reified T>
Iterable<*>.filterIsInstance(): List<T> {
val destination = mutableListOf<T>()
for (element in this) {
if (element is T) {
destination.add(element)
}
}
return destination
}
실체화한 타입 파라미터의 제약이 있다.
다음과 같은 경우에 실체화한 타입 파라미터를 사용할 수 있다.
- 타입 검사와 캐스팅(is, !is, as, as?)
- 10장에서 설명할 코틀린 리플렉션 API(::class)
- 코틀린 타입에 대응하는 java.lang.Class를 얻기(::class.java)
- 다른 함수를 호출할 때 타입 인자로 사용
하지만 다음과 같은 일은 할 수 없다.
- 타입 파라미터 클래스의 인스턴스 생성하기
- 타입 파라미터 클래스의 동반 객체 메서드 호출하기
- 실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
- 클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정하기
9.3 변성: 제네릭과 하위 타입
변성(variance) 개념은 List<String>와 List<Any>와 같이 기저 타입이 같고 타입 인자가 다른 여러 타입이 서로 어떤 관계가 있는지 설명하는 개념이다.

fun test(i: Int) {
val n: Number = i
fun f(s: String) {
println(s)
}
// f(i) // 컴파일 에러
}
위 코드는 허용된다. 상위 타입이 더 넓은 범위이기 때문. 하지만 String은 Int의 상위 타입이 아니기 때문에 안된다. 이런 상하 관계가 있다.
null도 ?가 더 넓은 범위라서 ?가 있는게 상위타입임.

이후에 공변성에 대해 나오는데 그냥 사진 찍어둔것만 조금 올림.
나중에 작성하기
공변성 | 반공변성 | 무공변성 |
Producer<out T> | Consumer<in T> | MutableList<T> |
타입 인자의 하위 타입 관계가 제네릭 타입에서도 유지된다. | 타입 인자의 하위 타입 관계가 제네릭 타입에서 뒤집힌다. | 하위 타입 관계가 성립되지 않는다. |
Producer<Cat>은 Producer<Animal>의 하위 타입이다. | Consumer<Animal>은 Consumer<Cat>의 하위 타입니다. | |
T를 아웃 위치에서만 사용할 수 있다. | T를 인 위치에서만 사용할 수 있다. | T를 아무 위치에서나 사용할 수 있다. |



'CS > 코틀린 인 액션' 카테고리의 다른 글
11. DSL 만들기 (0) | 2023.05.16 |
---|---|
10. 애노테이션과 리플렉션 (0) | 2023.05.10 |
8. 고차 함수: 파라미터와 반환 값으로 람다 사용 (0) | 2023.05.05 |
7. 연산자 오버로딩과 기타 관례 (0) | 2023.05.03 |
6. 코틀린 타입 시스템 (0) | 2023.04.30 |