티스토리 뷰

kotlin

[kotlin/코틀린] 제네릭

hrniin 2023. 7. 19. 23:30
728x90

 

제네릭

 

  1. 제네릭 선언
  • 제네릭 선언을 위해서는 하나 이상의 타입 파라미터를 추가해야 한다.
  • 선언을 사용할 때는 실제 타입을 지정한다.

 

open class P<T>(val t: T)

class Box<T>(t: T): P<T>(t) // 생성자 위임 호출 시 타입 인자 추론 x. 생략할 수 없음

fun main() {
    val box1 = Box<Int>(1)
    val box2 = Box(1) // 생성자 호출 시 타입 인자 생략 o
}
  • 컴파일러가 타입을 추론할 수 있으면 타입 인자 생략 가능 (자바와 달리 <>까지 생략)
  • 부모 생성자를 위임 호출할 때는 타입 인자를 추론해주지 못하므로 타입 인자를 꼭 명시해야 한다.
  • 클래스 안에서는 타입 파라미터를 프로퍼티/함수의 타입, 다른 제네릭 선언의 타입으로 사용할 수 있다.

 

 

 

      2. 바운드

fun <T : Comparable<T>> sort(list: List<T>) { ... }

fun main() {
    sort(listOf(1, 2, 3)) // ok
    sort(listOf(HashMap<Int, String>()) // error
}
  • 타입 인자에 들어갈 수 있는 타입에 아무런 제약이 없다면, Any?와 동일하다.
  • 특정 타입과 그 하위 타입으로만 타입 인자에 사용될 수 있도록 제한 → 상위 바운드(upper bound)

 

fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    return list.filter { it > threshold }.map { it.toString() }
}
  • where 키워드를 통해 상위 바운드를 여러개 지정할 수 있다.

 

 

 

      3. 타입 구체화

  • 타입 소거(type easer): 제네릭 타입 인자의 정보는 런타임에 알 수 없다. is/as 연산자를 사용할 수 없다.
    • 예를 들어 Foo<Bar>나 Foo<Baz?> 등이 Foo<*>로 변경됨.

 

inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
    val destination = mutableListOf<T>()
    for (element in this) {
        if (element is T) { // 타입 소거되지 않으므로 is 연산자를 통해 타입 검사 가능
            destination.add(element)
        }
    }

    return destination
}

fun main() {
    val items = listOf("one", 2, "three")
    println(items.filterIsInstance<String>()) // [one, three]
}
 
  • 구체화: 제네릭 타입 인자의 정보를 런타임까지 유지한다.
    • 인라인 함수에 대해서만 사용할 수 있다.
    • 함수 본문을 호출 위치로 인라인시키기 때문에 실제 타입을 항상 알 수 있다.
    • reified 키워드로 타입 파라미터를 지정한다.

 

 

 

      4. 변성

  • 무공변(invariant)
    • 타입 파라미터의 하위 타입 관계와 상관없이, 제네릭 타입 사이에는 하위 타입 관계가 존재하지 않음.
    • in/out 키워드 없이 제네릭 타입을 선언하면 모두 무공변.

 

// 타입 파라미터 T가 항상 out 위치에서만 사용됨
interface LazyList<out T> {
    fun get(index: Int): T
     
    fun subList<range: IntRange): LazyList<T>
     
    fun getUpTo(index: Int): () -> List<T>
}
 
  • 공변성(covariant): 하위 타입 관계를 유지
    • A가 B의 하위 타입일 때, Foo<A>가 Foo<B>의 하위 타입이면 Foo는 공변적.
    • 타입 파라미터를 out으로 선언 → 생산자 역할
    • T 타입의 값을 반환하기만 하고 T 타입의 값을 입력으로 받지는 않음.

 

// 타입 파라미터 T가 항상 in 위치에서만 사용됨
interface Writer<in T> {
    fun write(value: T)
     
    fun writeList(values: Iterable<T>)
}
 
  • 반공변성(contravariant): 뒤집한 하위 타입 관계
    • A가 B의 하위 타입일 때, Foo<B>가 Foo<A>의 하위 타입이면 Foo는 반공변적.
    • 타입 파라미터를 in으로 선언 → 소비자 역할
    • T 타입의 값을 입력으로 받기만 하고 T 타입의 값을 반환하지는 않음.

 

  • 사용 지점 변성 
    • 제네릭 타입을 사용하는 위치에서 in/out을 사용 → 프로젝션
fun copy(from: Array<Any>, to: Array<Any>) {
    for (i in from.indices)
        to[i] = from[i]
}

fun main() {
    val ints: Array<Int> = arrayOf(1, 2, 3)
    val any = Array<Any>(3) { "" }
    copy(ints, any)
}
 
  • Array가 무공변이므로 Array<Any>에 Array<Int>의 값을 대입할 수 없음

 

fun copy(from: Array<out Any>, to: Array<Any>) {
    for (i in from.indices)
        to[i] = from[i]
}

fun main() {
    val ints: Array<Int> = arrayOf(1, 2, 3)
    val any = Array<Any>(3) { "" }
    copy(ints, any)
}
 
  • 사용 지점 변성을 통해 해결 
  • from의 타입 파라미터를 out으로 변경

 

  • 스타 프로젝션
    • *로 표시
    • 타입 파라미터의 바운드 안에서 아무 타입이나 될 수 있다는 의미.
    • Foo<Any?>: 아무 타입의 값이 들어갈 수 있음 ↔ Foo<*>: 어떤 타입인지 알려져 있지 않음
interface Producer<out T> { ... }

interface Consumer<in T> { ... }

fun main() {
    val producer: Producer<*> // Producer<Any?>와 동일
    val consumer: Consumer<*> // Consumer<Nothing>과 동일
}
    • 선언 지점 변성이 붙은 타입 파라미터를 대신하여 쓸 수 있다.

 

 

728x90
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함