퉁탕퉁탕 만들어보자

Kotlin Generic (in, out) 본문

Computer/Kotlin

Kotlin Generic (in, out)

호숀티 2022. 4. 23. 23:00
반응형

Kotlin의 Generic은 기본적으로 Java와 동일한 방식으로 사용가능합니다.

class Box<T>(t: T) {
	var value = t
}

val box: Box<Int> = Box<Int>(1)

val box = Box(1)

명시적으로 타입을 써줄수도 있지만, 추론이 가능한경우에는 그냥 값을 넣어줄 수도 있습니다.

 

Java의 WildCard type

Java type 시스템에는 와일드카드 type이 있습니다.

  • <? extends T>
    • T 또는 를 상속받는 type만 가능.
    • T로 읽기(리턴타입)가능. T로 인수(argument) 전달 불가능
  • <? super T>
    • T 또는 T의 super type만 가능.
    • T로 읽기(리턴타입) 불가능. T로 인수(argument)전달 가능.

 

T를 매개변수로 사용하는 메서드가 없고 T를 반환하는 메서드만 있는 일반 인터페이스 Source<T>가 있다고 가정해 보겠습니다.

// Java
interface Source<T> {
    T nextT();
}

그런 다음 Source<Object> 유형의 변수에 Source<String> 인스턴스에 대한 참조를 저장하는 것은 완벽하게 안전합니다. 호출할 소비자 메서드가 없습니다. 그러나 Java는 이것을 알지 못하며 여전히 금지합니다.

// Java
void demo(Source<String> strs) {
    Source<Object> objects = strs; // !!! Not allowed in Java
    // ...
}

이 문제를 해결하려면 Source<? extends Object>를 선언해야 합니다. 이렇게 하는 것은 의미가 없습니다. 이전과 같은 변수에 대해 동일한 메서드를 모두 호출할 수 있으므로 더 복잡한 유형에 의해 추가되는 값이 없기 때문입니다. 그러나 컴파일러는 그것을 모릅니다.

Kotlin에는 이런 종류의 것을 컴파일러에게 설명하는 방법이 있습니다. 이를 declaration-site variance라고 합니다. Source<T>의 멤버에서만 반환(생성)되고 소비되지 않도록 Source의 형식 매개 변수 T에 주석을 추가할 수 있습니다. 이렇게 하려면 out 을 사용합니다.

// KOTLIN
interface Source<out T> {
    fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // This is OK, since T is an out-parameter
    // ...
}

클래스 C의 type 매개변수 T가 out으로 선언되면 C 멤버의 out-position에서만 발생할 수 있지만 그 대가로 C<Base>는 C<Derived>의 안전하게 상위 유형이 될 수 있습니다.
C는 T의 소비자가 아니라 T의 생산자라고 생각할 수 있습니다.

out 외에도 Kotlin은 in 도 제공합니다. 소비만 가능하고 생산은 절대 불가능합니다. 좋은 예는 Comparable입니다.

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

fun demo(x: Comparable<Number>) {
    // 1.0 은 double 타입임, Number의 subtype임. 하위 타입으로 인수 받는것이 가능
    x.compareTo(1.0) 
  
    // 따라서 x(Comparable<Number>) 상위타입의 객체를 y(Comparable<Double>) 하위타입에 assign이 가능하다.
    val y: Comparable<Double> = x // OK!
}

 

Type projections

use-site-variance: type 예측
유형 매개변수 T를 out으로 선언하고 사용하는 곳에서 하위타입 입력 문제를 피하는 것은 매우 쉽지만 일부 클래스는 실제로 T만 반환하도록 제한할 수 없습니다! 이에 대한 좋은 예는 Array입니다.

class Array<T>(val size: Int) {
    operator fun get(index: Int): T { ... }
    operator fun set(index: Int, value: T) { ... }
}
fun copy(from: Array<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

이 함수는 한 배열에서 다른 배열로 항목을 복사해야 합니다. 실제로 적용해 보겠습니다.

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any)
//   ^ type is Array<Int> but Array<Any> was expected

여기에서 친숙한 문제가 발생합니다. Array<T>는 T에서 불변이므로 Array<Int>나 Array<Any> 모두 다른 쪽의 하위 유형이 아닙니다. 왜 안 돼? 왜냐하면 copy가 예기치 않은 동작을 가질 수 있기 때문입니다. 예를 들어, 문자열을 from에 쓰고 실제로 Int 배열을 전달하면 나중에 ClassCastException이 발생합니다.

 

복사 기능이 from에 쓰는 것을 금지하려면 다음을 수행할 수 있습니다.

fun copy(from: Array<out Any>, to: Array<Any>) { ... }

이것은 type projection이며, 이는 from이 단순한 배열이 아니라 제한된(투영된) 배열임을 의미합니다. 형식 매개변수 T를 반환하는 메서드만 호출할 수 있습니다. 이 경우 get()만 호출할 수 있습니다. 이것은 use-site 편차에 대한 우리의 접근 방식이며 Java의 Array<? extends Object> 는 약간 더 단순합니다.

in을 사용하여 유형을 투영할 수도 있습니다.

fun fill(dest: Array<in String>, value: String) { ... }

Array<in String>은 Java의 Array<? super String>에 대응됩니다. 이것은 CharSequence 배열 또는 Object 배열을 fill() 함수에 전달할 수 있음을 의미합니다.

 

Star-projections(*)

Kotlin은 스타 프로젝션 구문을 제공합니다.

Foo<out T : TUpper>의 경우 T는 상한 TUpper가 있는 공변 유형 매개변수이며 Foo<*>는 Foo<out TUpper>와 동일합니다. 이것은 T를 알 수 없을 때 Foo<*>에서 TUpper의 값을 안전하게 읽을 수 있음을 의미합니다.

Foo<in T>의 경우 T가 반공변 유형 매개변수인 경우 Foo<*>는 Foo<in Nothing>과 동일합니다. 이것은 T를 알 수 없을 때 안전한 방법으로 Foo<*>에 쓸 수 있는 것이 없음을 의미합니다.

Foo<T : TUpper>의 경우, T는 상한 TUpper가 있는 불변 유형 매개변수이며, Foo<*>는 값 읽기의 경우 Foo<out TUpper> 및 값 쓰기의 경우 Foo<in Nothing>과 동일합니다.

제네릭 형식에 여러 형식 매개 변수가 있는 경우 각 매개 변수를 독립적으로 프로젝션할 수 있습니다. 예를 들어 유형이 인터페이스 Function<in T, out U>로 선언된 경우 다음 스타 프로젝션을 사용할 수 있습니다.

  • Function<*, String>은 Function<in Nothing, String>을 의미합니다.
  • Function<Int, *>는 Function<Int, out Any?>를 의미합니다.
  • Function<*, *>은 Function<in nothing, out Any?>를 의미합니다.

Star-projections는 Java의 원시 유형과 매우 유사하지만 안전합니다.

 

Generic functions

클래스는 형식 매개변수를 가질 수 있는 유일한 선언이 아닙니다. function도 가능합니다. 유형 매개변수는 함수 이름 앞에 배치됩니다.

fun <T> singletonList(item: T): List<T> {
    // ...
}

fun <T> T.basicToString(): String { // extension function
    // ...
}

 

일반 함수를 호출하려면 호출하는 곳에서 함수 이름 뒤에 형식 인수를 지정합니다.

val l = singletonList<Int>(1)

 

컨텍스트에서 유추할 수 있는 경우 형식 인수를 생략할 수 있으므로 다음 예제도 작동합니다.

val l = singletonList(1)

 

Generic constraints

주어진 type 매개변수를 대체할 수 있는 모든 가능한 type 집합은 일반 제약 조건에 의해 제한될 수 있습니다.

상한
가장 일반적인 유형의 제약 조건은 Java의 extends 키워드에 해당하는 상한입니다.

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

콜론 뒤에 지정된 형식은 상한으로, Comparable<T>의 하위 형식만 T를 대체할 수 있음을 나타냅니다. 예를 들면 다음과 같습니다.

sort(listOf(1, 2, 3)) // OK. Int is a subtype of Comparable<Int>
sort(listOf(HashMap<Int, String>())) // Error: HashMap<Int, String> is not a subtype of Comparable<HashMap<Int, String>>

기본 상한(아무 것도 지정되지 않은 경우)은 Any?입니다. 꺾쇠 괄호 안에는 하나의 상한만 지정할 수 있습니다. 동일한 유형 매개변수에 둘 이상의 상한이 필요한 경우 별도의 where 절이 필요합니다.

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 절의 모든 조건을 동시에 충족해야 합니다. 위의 예에서 T 유형은 CharSequence와 Comparable을 모두 구현해야 합니다.

 

 

* https://kotlinlang.org/docs/generics.html 

728x90
반응형

'Computer > Kotlin' 카테고리의 다른 글

Dokka 에서 SourceSet 설정하기  (0) 2023.05.14
Multi Module 프로젝트에서 Dokka 적용하기  (0) 2023.05.13
Sealed class  (0) 2022.04.17
Data class  (0) 2022.04.17
loops  (0) 2022.03.31