Generics

자바처럼 타입 파라매터가 있다

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

클래스의 인스턴스 작성

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

생성자의 인자나 다른 방법으로 파라매터의 타입이 추론가능한 경우 생략가능

val box = Box(1) // 1 은ㅇ Int 타입, 컴파일러는 Box<Int>라고 안다

가변 (Variance)

자바 타입 시스템에서 가장 어려운 부분 중 하나가 와일드 카드 타입 (Java Generics FAQ 참고) 하지만, 코틀린에는 없는 대신 선언-위치 가변(declaration-site variance)과 타입 프로젝션 (type projections)이 존재

자바에서 와일드 카드가 필요한 이유

Effective Java Item 28:bound bounded wildcard 를 사용해서 API의 유연성을 높이기 위함

자바의 Generic 타입은 불변(invariant). List<String>List<Object>의 서브 타입이 아니다. List가 불변이 아닌 경우, 다음 코드가 컴파일되어 실행시에 예외가 발생하므로 Java 배열보다 좋지않다

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! 위에서 말한 문제의 원인. Java가 이것을 금지!
objs.add(1); // 여기에 Integer를 String 리스트에 넣는다
String s = strs.get(0); // !!! ClassCastException: Integer를 String으로 캐스트 불가

Java는 실행시의 안전을 보장하기 위해 위와 같은 것을 금지. 하지만, 이것은 몇가지의 의미가 있다. 예를들어, 인Collection 인터페이스 addAll() 메소드의 시그니처는 직관적으로 다음과 같이 작성할 수 있다

// Java
interface Collection<E> ... {
  void addAll(Collection<E> items);
}

하지만, 다음과 같은 간단한 것은 불가능 (완전히 안전하다)

// Java
void copyAll(Collection<Object> to, Collection<String> from) {
  to.addAll(from); // !!! addAll의 선언으로는 컴파일되지 않는다
                   //     Collection<String>은 Collection<Object>의 서브 타입이 아니다
}

Effective Java Item 25:Prefer lists to arrays 를 참고

실제 addAll()의 시그니처는 다음과 같다

// Java
interface Collection<E> ... {
  void addAll(Collection<? extends E> items);
}

와일드 카드 타입 인자 ? extends T 는 이 메소드가 T 자체가 아닌 T의 하위 유형의 객체 컬렉션을 허용한다는 의미. 이를 해소하기 위해 Collection<String>Collection<? extends Object>의 하위 타입이라고 추가. "전문 용어"로 extends-bound (upper bound)를 가지는 타입 공변(covariant) 을 만듬

이 트릭이 왜 효과가 있는지 핵심은 단순하다. 컬렉션에서 아이템을 가져올수 있다면 String 컬렉션을 사용해 Object 를 읽을 수 있다. 반대로 컬렉션에 항목을 넣을 수 있다면 Object 콜렉션에 String을 넣을 수 있다. Java에서는 List<? super String>List<Object>의 상위타입이다.

후자는 역변(contravariance)이라고 부르며 List<? super String>에 인자로 String을 받는 메소드만 호출 가능 (예. add(String)이나 set(int, String)을 호출할 수 있다). 하지만 List<T>에서 T를 반환하는 것을 호출하면 String이 아니라 Object를 얻음

Joshua Bloch는 이 객체는 생산자(Producers)에서 읽고, 소비자(Consumers)에서 쓸 수 있다고 말함. "유연성을 극대화하기 위해 생산자 또는 소비자를 나타내는 파라미터에 와일드 카드 타입을 사용하라"고 권한다.

PECS는 Producer-Extends、Consumer-Super의 약자이다.

주의 : 생산자 객체를 사용하는 경우 List<? extends Foo> 에 대해 add()set()을 호출할 수 없지만, 이 객체는 불변(immutable)이라는 것을 의미하지않는다. 예를들어 clear()는 매개변수를 사용하지 않으므로, List에서 모든 항목을 제거 가능. 와일드 카드 (또는 다른 타입의 가변(variance))로 보장되는 것은 타입 안전이다. 불변은 완전히 다른 이야기이다.

선언-위치 가변(Declaration-site variance)

T를 파라매터로 받으며 메소드는 없고 제네릭 인터페이스 Source<T>가 있고 T를 반환하는 메소드만 있다고 가정

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

Source<Object> 타입 인스턴스에 Source<String> 타입 변수를 저장하는 것은 완전히 안전하다. 호출될 소비자도 없다. 하지만, 자바는 이를 알지못해 이것을 금지하고 있다.

// Java
void demo(Source<String> strs) {
  Source<Object> objects = strs; // !!! 자바에서 허용하지 않음
  // ...
}

이 코드의 문제를 고치려면 Source<? extends Object> 타입 객체를 선언해야 하는데 이는 다소 의미가 없다. 왜냐면 수정 전에도 동일한 메서드를 호출할 수 있고 더 복잡한 타입으로 수정해도 값을 추가하지 않기 때문이다. 하지만 컴파일러는 이를 알지 못한다.

Kotlin에서는 컴파일에 이런 내용을 설명하는 방법이 존재. 이를 선언-위치 가변(Declaration-site variance)라고 부르고 소스의 파라미터 T에 어노테이션을 붙여 Source<T>의 멤버가 반환(생성)되고, 소비되지않는다고 할 수 있다. 이를 위해 out 제한자를 지정

abstract class Source<out T> {
    abstract fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // T는 out 파라미터이므 이것은 OK
    // ...
}

일반적인 규칙 : 클래스 C의 파라매터 타입 T가 선언되면 C 멤버에서 out-위치에서만 발생할 수 있지만, 반환값으로 C<Base>는 안전하게 C<Derived>의 상위 타입이 될 수 있습니다.

전문용어로 클래스 C는 매개 변수 T공변(covariant)이거나 T공변(covariant) 타입 매개 변수라고 말한다.

CT의 소비자가 아니라 T의 생잔자로 생각가능

out 제한자는 가변(variance) 어노테이션으로 불리며, 타입 매개 변수 선언 위치에서 제공. 이것은 타입 사용의 와일드 카드가 타입을 공변으로 만드는 Java와 사용-위치 가변(use-site variance)과 대조된다.

이외에도 Kotlin은 대체 가변 어노테이션인 in 을 제공. 즉, 타입 매개 변수가 역변(contravariant)하게 만든다 즉, 소비될 수만 있고 생산은 안된니다.

abstract class Comparable<in T> {
    abstract fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0은 Number의 하위 타입인 Double 타입을 갖는다
    // 따라서 Comparable<Double> 타입의 변수에 x를 할당 가능
    val y: Comparable<Double> = x // OK!
}

The Existential Transformation: Consumer in, Producer out! :-)

타입 프로젝션 (Type projections)

사용-위치 가변(Use-site variance) : 타입 프로젝션

타입 파라미터 T를 out으로 선언하는 것은 매우 편리하고 사용 위치에서 하위타입 관련 문제가 없다. 그런데 실제로 T를 리턴하도록 제한할 때 할 수 없다면 어떻게 될까?

class Array<T>(val size: Int) {
    fun get(index: Int): T { /* ... */ }
    fun set(index: Int, value: T) { /* ... */ }
}

이 클래스는 T에서 공변(covariant)역변(contravariant)이 될 수 없다. 그리고 특정 유연성을 부과한다. 다음 함수를 보자.

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) // Error: expects (Array<Any>, Array<Any>)

익숙한 문제 발생 : Array<T>T에 대해 불변(invariant)하므로 Array<Int>Array<Any>는 다른 타입의 서브 타입이 아니다. 다시 말해, copy가 나쁜 일을 할지 모르기 때문이다. 즉, String을 from에서 쓰려고 시도 할 수 있으며, 실제로 여기에 Int 배열을 전달하면 나중에 ClassCastException이 던져질 것이다.

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

여기에서 사용한 것을 타입 프로젝션(type projection)이라고 한다. from은 단순한 배열이 아니라 제한된 (projected) 것이다. 타입 파라매터 T를 반환하는 메소드만 호출 가능하다. 즉, get() 만 호출할 수 있다. 이것이 사용-위치 가변(use-site variance) 를 위한 Kotlin의 접근 방법이고, Java의 Array<? extends Object>에 대응하지만, 더 간단하다.

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

Array<in String>은 Java의 Array<? super String>에 대응한다. 즉, CharSequence의 배열이나 Object의 배열을 fill() 함수로 넘길 수 있다.

Star-projections

가끔 타입 인자에 대해 알지 못하지만, 안전한 방식으로 사용하고 싶을 경우, 안전한 방법은 제네릭 타입의 프로젝션을 정의하는 것이다. 즉, 제네릭 타입의 모든 구체적인 인스턴스화하는 것으로 해당 프로젝션의 서브 타입이 된다.

Star-projections 구문

  • Foo<out T>의 경우, T는 상한 TUpper를 가진 공변(covariant) 타입의 파라매터이면 Foo<*>Foo<out TUpper>와 같다. 이것은 T가 불명확한 경우 안전하게 Foo<*>에서 TUpper의 값을 읽을 수 있다는 의미
  • Foo<in T>의 경우, T가 역변(contravariant) 타입 파라매터이면 Foo<*>Foo<in Noting>과 간다. 이는 T는 불명확한 경우 안전하게 Foo<*>에 쓸수 없다는 것을 의미
  • Foo<T>의 경우, T는 상한 TUpper를 가지는 무공변(invariant) 타입의 파라매터이면 Foo<*>는 값을 읽을 때는 Foo<out TUper>와 같고, 쓸 때는 Foo<in Noting>과 같다

제네릭 타입이 몇가지의 타입 파라매터를 가지는 경우, 각각 독립적인 프로젝션할 수 있다. 예를들면, 타입이 interface Function<in T, out U>로 선언되어있다면 다음과 같은 Star-projection을 생각할 수 있다

  • Function<*, String> Function<in Nothing, String> 을 의미
  • Function<Int, *>Function<Int, out Any?> 을 의미
  • Function<*, *>Function<in Nothing, out Any?> 을 의미

주의 : Start-projection은 상당히 Java의 raw 타입과 유사하지만, 안전하다

Generic 함수

타입 파라매터를 가질 수 있는 것은 클래스뿐만 아니라 함수도 가능. 타입 파라매터는 함수 이름 앞에 위치한다

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

fun <T> T.basicToString() : String {  // 확장함수
  // ...
}

제네릭 함수를 호출하기 위해서는 함수명 뒤에 타입 인자를 지정

val l = singletonList<Int>(1)

Generic 제약

주어진 타입 파라매터로 대체될 수 있는 모든 가능한 타입은 제네릭 제약에 의해 제한된다.

상한 (Upper bounds)

가장 일반적인 제약은 Java의 extends 키워드에 대응하는 상한(upper bound)이다.

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

콜론 뒤에 지정된 타입이 상한(upper bound)이다. Comparable<T>의 서브 타입은 T를 대신하여 사용할 수 있다.

sort(listOf(1, 2, 3)) // Int는 Comparable<Int>의 서브 타입
sort(listOf(HashMap<Int, String>())) // 에러: HashMap<Int, String> 은 Comparable<HashMap<Int, String>> 의 서브 타입이 아니다

기본적인 상한 (지정하지 않는 경우에도) 은 Any? 이다. 괄호안에하나의 상한만을 지정할 수 있다. 동일한 타입 파라매터에 복수 상한이 필요한 경우, 각각 별도의 where 구문이 필요하다

fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
    where T : Comparable,
          T : Cloneable {
  return list.filter { it > threshold }.map { it.clone() }
}

results matching ""

    No results matching ""