오늘도 더 나은 코드를 작성하였습니까?

제네릭스 본문

Kotlin in Action/코틀린 답게 사용하기

제네릭스

hik14 2021. 2. 26. 14:54

제네릭스를 깊이 다루면서,

 

실체화된 타입 파라미터(reified type parameter)

 

 - 실체화된 타입 파라미터를 사용하면 인라인 함수 호출에 있어 타입 인자로 쓰인 구체적인 타입을 실행 시점에서 알수있다.(일반적인 클래스나 함수의 경우 타입 인자 정보가 실행 시점에 사라져버리기 때문에 이런것이 불가능하다.)

 

선언 지점 변성(declaration-site variance)

- 선언 지점 변성을 사용하면 기저 타입은 같지만 타입 인자가 다른 두 제네릭 타입 Type<A> Type<B>가 있을때, 타입인자 A B 상위/하위 타입 관계에 따라 두 제네릭 타입의 상위/하위 타입관계 어떻게 되는지 지정할 수 있다.

예를 들면 List<Any>를 인자로 받는 함수에게 List<Int>타입의 값을 전달할 수 있을지 여부는 선언 지점 변성을 통해 지정할 수있다.

 

* 기저타입 -  List 와 List<Stirng> 여기서 타입 파라미터를 제외한 List를 뜻한다.

 

사용지점 변성(use-site variance) 자바의 와일드 카드

 

- 같은 목표 (제네릭 타입 값 사이의 상위/하위 타입 관계 지정)을 제네릭 타입 값을 사용하는 위치에서 파라미터 타입에 대한 제약 표시한다.

 

제네릭 타입 파라미터

제네릭스 사용하려면 타입 파라미터를 받는 타입을 정의하고 제네릭 타입의 인스턴스를 만들려면 타입 파라미터에 구체적인 타입인자로 치환해야된다.

 

코틀린은 보통 타입과 동일하게 타입 파라미터의 타입도 추론한다.

val authors = listOf("Dmitry", "Svetlana")

컴파일러는 List가 List<String>임을 추론한다.

반면, 비어있는 리스트 생성시에는 추론이 불가능 하기 때문에 명시를 해주어야한다. 

val readers: MutalbleList<String> = mutableListOf() // 변수타입 지정.

val readers = mutableListOf<String>() // 함수의 타입 파라미터 지정. 

 

*자바의 경우는 제네릭스가 추후 도입되어 이전 자바 버전과의 호환성을 위해 리스트의 원소타입을 지정하지 않고 생성이 가능하나,

코틀린은 처음부터 제네릭스가 도입해서 타입 인자를 반드시 정의 해야된다.

 

제네릭 함수와 프로퍼티

리스트를 다루는 함수를 작성한다면, 특정 타입을 저장하는 리스트뿐아니라 모든 리스트를 다룰 수 있는 함수를 원 할 것이다.

이럴때 제네릭스 함수를 작성해야 한다.

 

 

fun  <T>타입파라미터 선언 List<T>.slice(indices: IntRange) 수신객체 및 함수 파라미터:  List<T> (반환타입) 

println(letters.slice(0..2)) // 리스트의 타입을 고려해 추론
println(letters.slice<Char>(10..13)) // 명시적인 호출.

 

 

fun <T> List<T>.filter( predicate: (T) -> Boolean ):List<T>

val authors = listOf("Dmitry", "Svetlana")
val readers = mutableListOf("Dmitry", "Alice", "hik")

fun main() {
    println(
        readers.filter { it !in authors }
    )
}

filter 함수의 파라미터인 ( predicate: (T) -> Boolean )은 제네릭 타입인 it을 가진다. 

컴파일러는 filter함수가 List<T>타입에 호출할 수 있고 filter수신객체인 readers가 List<String>인것을 알고 여기서 <T>가 <String>인것을 추론한다. 

 

클래스나 인터페이스 안에 정의된,

메소드 확장함수,

최상위 함수에서는 타입 파라미터를 선언할수있다. 

아래 두 함수의 선언은 수신객체타입과 파라미터 타입에 타입파라미터를 사용하였다.

 

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

fun  <T> List<T>.filter( predicate: (T) -> Boolean ): List<T>

 

 

함수와 같이 제네릭 확장 프로퍼티를 선언할 수 있다.

val <T> List<T>.penultimate: T
    get() = this[size-2]
    
fun main() {
    println(listOf(1,2,3,4,5).penultimate)
}

확장프로퍼티만 제네릭 하게 만들수 있다. 일반 프로퍼티는 타입 파라미터를 가질 수 없다. 

 

제네릭 클래스 선언

 

자바와 마찬가지로 코틀린에서도 제네릭 클래스를 생성할 수 있는데 타입 파라미터를 이름 뒤에 붙이고 나면 클래스 본문 안에서 타입 파라미터를 다른 일반 타입처럼 사용할 수 있다. 표준 자바 List 인터페이스를 만들어보자

 

interface List<T>{

    operator fun get(index: Int): T

}

 

제네릭 클래스를 확장하는 클래스를 정의하려면 기반 타입의 제네릭 파라미터에 대해 타입인자를 지정해야된다.

 

1. 구체적인 타입 파라미터 타입을 지정하기.

 

2. 타입 파라미터로 받은 타입을 지정하기.

interface List<T>{

    operator fun get(index: Int): T

}

class StringList: List<String>{
    override fun get(index: Int): String {
        TODO("Not yet implemented")
    }
}

class ArrayList<T>: List<T>{
    override fun get(index: Int): T {
        TODO("Not yet implemented")
    }
}

 

2번째의 경우 List<T>의 T와 ArrayList<T>의 T는 전혀 다른 타입이다. 다른 문자를 사용해도 전혀 상관이없다.

심지어 클래스가 자신의 타입을 타입인자로 사용가능하다. 대표적인 예가 Comparable이다.

interface Comparable<T>{
    fun compareTo(other:T): Int
}

class String: Comparable<String>{
    override fun compareTo(other: String): Int {
        TODO("Not yet implemented")
    }
}