본문 바로가기

프로그래밍/kotlin

코틀린 - 제네릭

반응형

자바에서와 마찬가지로 코틀린의 클래스에는 유형 매개 변수가있을 수 있습니다.

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

 참조 ). 그리고 코틀린에는 와이드카드가 없습니다. 대신 선언-위치가변과 타입 프로젝션의 두가지를 제공합니다.먼저 자바가 왜 그와 같은 신비한 와일드 카드를 필요로하는지 생각해 봅시다. 이 문제는 

Effective Java

 , Item 28 : 

바운드 와일드 카드를 사용하여 API 유연성을 높이는

 방법에 설명되어 있습니다. 첫째, Java의 제네릭 형식은 

불변합니다

 . 즉 ,  

List<String>

List<Object>

 의 하위타입이 아님을 의미합니다.  List가 

불변

 적이 지 않으면 다음 코드가 컴파일되어 런타임에 예외가 발생 했으므로 자바의 배열보다 좋지 않습니다.

// 자바
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! 앞에서 언급한 문제 원이이 여기 있습니다. 자바는 이를 금지합니다.
objs.add(1); // 여기서 String의 리스트에 Integer를 넣는다.
String s = strs.get(0); // !!! ClassCastException: Interger를 String으로 변환할 수 없다.

그래서 자바는 런타임 안전을 보장하기 위해 이러한 것을 금지합니다. 그러나 이것은 몇 가지 함의를 가지고 있습니다. 예를 들어, 인터페이스 에서 

addAll()

메소드를 고려하십시오 

Collection

이 방법의 서명은 무엇입니까? 직관적으로, 우리는 그것을 다음과 같이 표현했습니다.

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

그러나 그때 우리는 다음과 같은 간단한 일을 할 수 없었을 것입니다 (그것은 완벽하게 안전합니다) :

// 자바
void copyAll(Collection<Object> to, Collection<String> from) {
  to.addAll(from); // !!! addAll의 단순한 선언으로는 컴파일되지 않는다.
                   //       Collection<String>은 Collection<Object>의 하위 타입이 아니다.
}

(Java에서 우리는이 교훈을 어려운 방법으로 배웠습니다. 

Effective Java

 , Item 25 : 

배열에 대한 목록 선호

 )그래서 실제 서명은 

addAll()

다음과 같습니다.

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

와일드카드 타입 인자 ? extends E 는 이 메서드가 E 자체가 아닌 E 의 하위타입의 객체 콜렉션을 허용한다는 것을 말합니다. 이는 items에서 안전하게 E 로 읽을 수 있지만(이 콜렉션의 요소는 E의 하위클래스의 인스턴스이다), E 의 어떤 하위타입인지 모르기 때문에 items에 쓸 수 없다는 것을 의미한다. 이런 제한을 해소하기 위해 Collection<String> 이 Collection<?extends Object> 의 하위타입이 되도록 기능을 추가했습니다. “전문 용어”로 extends-bound(upper bound)를 갖는 와일드카드를 사용해서 타입을 공변(convariant)으로 만들었습니다.이 트릭이 왜 작동하는지 이해하는데 있어 핵심은 다소 단순합니다. 만약 콜렉션에서 아이템을 가져올 수만 있다면 String 콜렉션에서 Object 를 읽는 것은 괜찮다. 역으로 콜렉션에 항목을 넣을 수만 있다면 Object 콜렉션에 String 을 넣는 건 괜찮습니다. 자바에서 List<? super String> 가 List<Object> 의 상위타입이 된니다.후자를 반공변(contravariance)이라 부르며, List<? super String> 에 인자로 String을 받는 메서드만 호출할 수 있습니다(예를 들어 add(String) 이나 set(int, String) 을 호출할 수 있다). 반면에 List<T> 에서 T 를 리턴하는 어떤 것을 호출하면 String 이 아닌 Object 를 얻게 된니다.Joshua Blochs는 이 객체는 Producer에서만 읽을 수 있고 Consumer로만 쓸 수 있다고 했습니다. Joshua Blochs는 “유연함을최대한 얻으려면 producer나 consumer를 표현하는 입력 파라미터에 와일드카드 타입을 사용하라“고 권하고 있으며, 다음과 같이기억을 쉽게 할 수 있는 약자를 제시했습니다.PECS는 Producer-Extends, Consumer-Super를 의미합니다.주의: producer 객체를 사용한다면, 예를 들어 List<? extends Foo> , 이 객체에 대해 add() 나 set() 을 호출하는 것을허용하지 않는다. 하지만 이것이 이 객체가 불변(immutable)인 것을 의미하는 것은 아니다. 예를 들어, 리스트의 모든 항목을 삭제하기 위해 clear() 를 호출하는 것은 가능합니다. 왜냐면, clear() 는 어떤 파라미터도 갖지 않기 때문이다. 와일드카드(또는 다

 

른 종류의 가변variance)가 보장하는 것은 타입 안정성이다. 불변은 완전히 다른 얘기입니다.

선언-위치 가변

매개 변수로 사용하는 

Source<T>

메서드가없는 일반 인터페이스가 있고 

T

반환하는 메서드 만 있다고 가정합니다 

T

.

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

그런 다음 

Source<String>

유형의 변수에 대한 인스턴스에 대한 참조를 저장하는 것이 완벽하게 안전합니다 

Source<Object>

. 호출 할 소비자 메소드가 없습니다. 하지만 자바는 이것을 모르고 아직도 그것을 금지하고있다.

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

이 문제를 해결하기 위해, 

Source<? extends Object>

이전과 같이 변수에 같은 메소드를 모두 호출 할 수 있기 때문에, 의미가없는 유형의 객체를 선언 해야합니다. 따라서 더 복잡한 유형으로 값을 추가 할 필요가 없습니다. 그러나 컴파일러는 그것을 모릅니다.Kotlin에는 컴파일러에게 이런 종류의 것을 설명하는 방법이 있습니다. 이를 

선언 - 사이트 차이

 라고합니다 Source 의 

type 매개 변수 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
    // ...
}

일반적인 규칙은 다음과 같습니다 유형 매개 변수 경우 

T

클래스가 

C

선언 

밖으로

 , 그것은에서만 발생할 수 있습니다 

밖으로

 의 구성원의 α 위치 

C

하지만, 대가로 

C<Base>

안전의 슈퍼 될 수 있습니다 

C<Derived>

."영리한 단어"그들은 클래스가 말할 

C

것입니다 

공변

 매개 변수에 

T

, 또는 그 

T

A는 

공변

 형식 매개 변수. 당신은 생각할 수 

C

있는 Being으로 

생산자

 의 

T

의, 아닌 

소비자

 의 

T

의.

아웃

 수정은이라고 

분산 주석을

 , 그리고 그것은 형식 매개 변수 선언 사이트에서 제공되기 때문에, 우리는 이야기 

선언 사이트 분산

 . 이것은 유형 사용의 와일드 카드가 유형을 공변으로 만드는 Java의 

사용 사이트 분산

 과 대조됩니다 .뿐만 아니라 

밖으로

 , 코 틀린은 보완 분산 주석을 제공 : 

 . 그것은 유형 매개 변수를 

반항적으로

 만든다 . 그것은 오직 소비되고 결코 생산 될 수 없다. contravariant 클래스의 좋은 예는 

Comparable

다음과 같습니다.

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!
}

우리가 믿는 그 단어 

에서

 와 

밖으로

 있는 자기 설명 따라서 위에서 언급 한 니모닉이 정말 필요하지 않습니다, (그들은 성공적으로 이미 꽤 많은 시간 동안 C #으로 사용했다), 그리고 하나가 더 높은 목적으로 바꿔 수 있습니다 :

Existential Transformation : Consumer in, 프로듀서 아웃! :-)

타입 프로젝션(Type projections)

사용-위치 가변 : 타입 프로젝션

이 같은 유형의 매개 변수 T를 선언하는 것이 매우 편리 

밖으로

 및 사용 사이트의 하위 유형에 문제를 피할 수 있지만, 일부 클래스는 

할 수없는

 사실만을 반환로 제한 

T

'을들! 좋은 예가 Array입니다.

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

이 클래스는 in 또는 contravariant 일 수 없습니다 

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

: 여기에서 우리는 같은 친숙한 문제로 실행 

Array<T>

되는 

불변

 에 

T

따라서도의, 

Array<Int>

그리고 

Array<Any>

 다른의 하위 유형입니다. 왜? 복사하기 때문에 다시 

수도

 나쁜 일을 할, 그것은 시도 할 수 있습니다 즉, 

쓰기

 , 말, 문자열을 

from

, 그리고 우리가 실제로의 배열을 전달하는 경우 

Int

가를, A는 

ClassCastException

언젠가 나중에 발생되었을 것입니다.그렇다면 우리가 보장하고자하는 유일한 것은 

copy()

나쁜 일은하지 않는다는 것입니다. 우리는에서 그것을 금지 할 

쓰기

 에 

from

, 우리는 할 수 있습니다

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

여기서 일어난 일은 

유형 투영

 이라고 불립니다 . 우리는 

from

단순히 배열이 아니라 제한된 ( 

투사 된

 ) 투영법이라고 말했습니다. 즉 , type 매개 변수를 반환하는 메서드 만 호출 할 수 있습니다 

T

.이 경우에는 호출 할 수 있음을 의미합니다 

get()

이것은 

site-variance에

 대한 우리의 접근 방식 이며, Java에 해당 

Array<? extends Object>

하지만 약간 간단한 방법입니다.당신이 가진 유형을 투사 할 수 

있는

 뿐만 아니라 :

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

Array<in String>

Java에 해당합니다 

Array<? super String>

. 즉 함수 

CharSequence

의 배열 또는 배열을 전달할 수 있습니다 .

Objectfill()

스타-프로젝션

가끔 형식 인수에 대해 알지 못하지만 안전한 방식으로 사용하고 싶다고 말할 수도 있습니다. 여기서 안전한 방법은 제네릭 형식의 프로젝션을 정의하는 것입니다. 즉, 제네릭 형식의 모든 구체적인 인스턴스화는 해당 투영의 하위 유형이됩니다.Kotlin 은이를 위해 

별 투영

 구문을 제공합니다.

  • 들면 Foo<out T>, 여기서 T상한와 공변 형 파라미터는 TUpperFoo<*>동일하다 Foo<out TUpper>그것은 때 것을 의미 T알 수 안전하게 수 읽기 의 값 TUpper에서을 Foo<*>.
  • 들면 Foo<in T>여기서 Tcontravariant 타입 파라미터이고, Foo<*>동일하다 Foo<in Nothing>그것은 당신이 수있는 건 아무것도 없다는 것을 의미 쓰기 를 Foo<*>할 때 안전한 방법으로 T알 수는.
  • 들면 Foo<T>, 여기서 T상한과 불변 타입 파라미터이고 TUpperFoo<*>동등 Foo<out TUpper>판독 값 및에 Foo<in Nothing>기록 값.

제네릭 형식에 여러 개의 형식 매개 변수가있는 경우 각 매개 변수는 독립적으로 프로젝션 될 수 있습니다. 예를 들어, 타입이 선언 

interface Function<in T, out U>

되면 다음과 같은 별 투영을 상상할 수 있습니다.

  • Function<*, String>수단 Function<in Nothing, String>;
  • Function<Int, *>수단 Function<Int, out Any?>;
  • Function<*, *>의미 Function<in Nothing, out Any?>.

참고

 : 별 투영법은 Java 원시 형식과 매우 비슷하지만 안전합니다.

지네릭 함수

클래스뿐만 아니라 형식 매개 변수를 가질 수 있습니다. 기능도 있습니다. 유형 매개 변수는 함수의 이름 앞에 위치합니다.

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

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

지네릭 함수를 호출하려면 함수 의 이름 

뒤에

 호출 사이트에서 유형 인수를 지정하십시오 .

val l = singletonList<Int>(1)

지네릭 제약

주어진 유형 매개 변수로 대체 할 수있는 모든 가능한 유형 세트는 

지네릭 제한 조건에

 의해 제한 될 수 있습니다 .

Upper bounds

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

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

콜론 다음에 지정된 유형은 upper bound

Comparable<T>

 입니다 

T

of의 하위 유형 만 대체 될 수 있습니다 예 :

sort(listOf(1, 2, 3)) // OK. Int는 Comparable<Int>의 하위타입이다.
sort(listOf(HashMap<Int, String>())) // Error: HashMap<Int, String>은  Comparable<HashMap<Int, String>>의 하위타입이 아니다.

디폴트의 ​​상한은 지정되어 있지 않은 경우입니다 

Any?

꺾쇠 괄호 안에는 하나의 상한 만 지정할 수 있습니다. 동일한 유형 매개 변수에 둘 이상의 상한이 필요하면 별도의 

where

 -clause 가 필요합니다 .

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

 

반응형

'프로그래밍 > kotlin' 카테고리의 다른 글

코틀린 - 위임  (0) 2021.07.13
코틀린 - 위임 프로퍼티  (0) 2021.07.13
코틀린 - sealed 클래스  (0) 2021.04.26
코틀린 - 데이터 클래스  (0) 2021.04.25
코틀린 - 확장  (0) 2021.04.25