9. 제네릭스
CS/코틀린 인 액션

9. 제네릭스

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를 아무 위치에서나 사용할 수 있다.