코틀린 언어 정리 7-3

Variance(불변 공변 반공변 개념과 코틀린)

코틀린에서의 불변, 공변, 반공변 개념 및 문법

코틀린에서는 공변과 반공변을 out / in 으로 나타냅니다. 불변은 in / out 없이 그냥 사용하면 불변입니다. 또 in / out 등을 사용한 위치에 따라 적용 범위가 변하고 동작이 달라질 수 있으므로 각각의 경우를 모두 알고 있는 것이 좋습니다.


사용 위치에 따른 분류 및 의미

Variance(변형, 타입 변형)

타입 변형은 제네릭 클래스 또는 인터페이스의 타입 매개변수 선언에 in / out을 적용하는 것을 말합니다.

Declaration-site variance( 선언측 변형 )

다음과 같이 클래스 등을 선언할 때 in / out을 명시하는 방법입니다.


예제

class ClassA < in T > { }

class ClassB < out T > { }


in

ClassA < in T >는 T 타입의 객체에 대한 입력은 허용하지만 출력은 허용하지 않습니다. 즉 메서드 등의 매개변수 타입으로는 사용 가능하지만 리턴 타입으로 사용할 수 없습니다.

out

ClassA < out T >는 T에 대한 모든 출력은 허용하지만 입력은 허용하지 않습니다. 즉 T를 메서드 등의 매개변수 타입으로 사용할 수 없고 리턴 타입으로는 사용 가능합니다.

주의

ClassA<in T>에 T타입의 매개변수를 입력받는 멤버 함수/메서드(ex. func1(obj: T) )가 있다고 가정하면, T 타입의 상위 타입을 입력 받을 수 있다는 의미가 아닙니다. 공변 반공변 개념은 제네릭 타입 자체에 적용되는 것이고 제네릭 타입의 타입 매개변수에 개별적으로 적용되지 않습니다.(T가 in으로 선언되었으므로 ClassA<Super> is a ClassA<Sub>의 관계가 성립하며, Super is a Sub의 관계가 성립하는 것이 아님)

적용 범위

Declaration-site variance에서의 in / out은 위에 설명한 의미 이외에 다음에 나올 Use-site variance에서의 in / out 의 의미도 포함합니다. 즉 제네릭 클래스의 타입 매개변수 선언에 in / out을 사용 했다면 이 제네릭 타입의 T에 실제 사용할 타입을 대입하여 함수 매개변수에 사용할 때에도 in / out이 적용된 것으로 처리됩니다.


Type projections(타입 투영)

변수 또는 함수의 매개변수에 제네릭 타입을 구체화(제네릭 타입 매개변수 T 등에 타입 인수를 입력)하여 선언하면서 타입 인수 앞에 in / out 또는 타입 인수에 * 을 사용하는 것을 말합니다.

Use-site variance( 사용 측 변형 )

예제

open class SuperA {}

class SubA: SuperA() {}


class ClassC < T > {}


fun func1 ( pobj1: ClassC < in SubA > ) { }

var obj1: ClassC < in SubA > = ClassC< SuperA >()


fun func2 ( pobj2: ClassC < out SuperA > ) { }

var obj2: ClassC < out SuperA > = ClassC< SubA >()


in

ClassC < in SubA >타입으로 선언된 func1 함수의 매개변수 pobj1, 그리고 변수 obj1에는 ClassC<SuperA>를 대입할 수 있습니다.

out

ClassC < out SuperA > 타입으로 선언된 func2 함수의 매개변수 pobj2, 그리고 변수 obj2에는 ClassC<SubA>타입의 객체를 대입할 수 있습니다.

적용 범위

매개변수나 변수 선언에 사용한 in / out은 이미 선언된 제네릭 타입 ClassC<T>에 적용되는 것이기 때문에 이 제네릭 타입을 매개변수로 전달 받을 때만 공변/반공변을 체크하며 이미 선언되어 있는 메서드나 프로퍼티에는 영향을 주지 않습니다.


Star-projections

제네릭 클래스에서 타입매개변수(ex. T)로 사용할 임의의 타입(T에 입력할 타입 인수)에 대해 잘 모를 때 타입 인수로 * 를 사용하면 모든 타입의 상위 타입으로 처리됩니다. 즉 ClassA<*> 은 in 위치(주로 매개변수 선언 위치)에서 최 하위 타입인 Nothing, out의 위치(주로 리턴 타입 선언 위치)에서는 최상위 타입인 Any? 로 처리됩니다. 따라서 타입매개변수만 다른 모든 동일한 제네릭 클래스의 사용이 가능합니다. 즉 사용할 타입의 상속 계층에 대해 모르거나 알고 싶지 않을 때, in / out을 어떻게 사용해야 할지 모를 때, 아니면 의도적으로 타입매개변수만 다른 모든 동일한 제네릭 클래스의 입력을 허용하고 싶을 때 만능키처럼 사용할 수 있습니다.


예제

class ClassC < T > {}


// Star-projection을 함수의 매개변수에 사용하는 경우

fun func1 ( obj: ClassC < * > ) { }


// Star-projection을 변수 선언에 사용하는 경우

var obj: ClassC < * > = ClassC< Any >()



요약

불변

제네릭의 타입 매개변수 선언 앞에 아무런 기호도 추가하지 않습니다. 제네릭 타입은 타입 매개변수가 다르면 기본적으로 서로 관련 없는 타입으로 처리됩니다.

공변

제네릭 타입이 타입 매개변수의 상속 관계를 그대로 가져갑니다. 선언하는 타입 앞에 out을 추가합니다.

반공변

제네릭 타입이 타입 매개변수의 상속 관계를 역으로 가져갑니다. 선언하는 타입 앞에 in을 추가합니다.

자동

in / out을 구분하여 추가하는 것이 번거로울 경우 선언하는 타입 앞에 * 을 추가합니다. 만능키처럼 동작합니다. 즉 선언한 제네릭 타입(T가 아니라 G<T>)이 항상 최상위 또는 최하위 타입으로 처리됩니다.

in / out 의미

공변과 반공변을 명시할 때 제네릭 클래스나 인터페이스 선언의 타입매개변수 앞(Declaration-site) 또는 제네릭 타입의 변수나 매개변수 선언의 타입 인수 앞(Use-site)에 사용하는 키워드입니다. 사용하는 위치, 즉 Declaration-site, Use-site에 따라 in / out의 적용 범위가 다릅니다.

먼저 이 후 설명을 위해 다음과 같은 클래스가 선언되어 있다고 가정하겠습니다.


SubA 는 SuperA를 상속 받는 관계입니다. 즉 SubA 는 SuperA의 하위 타입입니다.

Use-site

in, out을 변수, 매개 변수 선언등에 적용하는 경우입니다.

GenClassB<out T>

SubA와 SuperA의 관계가 제네릭 타입에도 적용됩니다.

GenClassB<SubA>는 GenClassB<SuperA>의 하위 타입처럼 처리됩니다.

GenClassA<in T>

SubA와 SuperA의 관계가 제네릭 타입에서 역전됩니다.

GenClassA<SuperA>는 GenClassA<SubA>의 하위 타입처럼 처리됩니다.

적용 범위

사용된 위치(변수, 매개변수 선언) 이 후부터 효과가 있으며 기존 선언(Declaration-site, 이전에 Use-site로 선언한 변수, 매개변수에 사용한 in / out / * 등)으로 만들어진 공변/반공변 규칙을 거스르지 못합니다.


Declaration-site

in, out을 제네릭 클래스 선언에 적용하는 경우입니다.

GenClassB<out T>

GenClassB<out T> 에서 T 타입의 객체는 GenClassB<out T>의 외부로 출력하는것만 가능합니다. 즉 GenClassB<out T>에 선언된 메서드(멤버함수)의 매개변수에는 T타입을 사용할 수 없으며 리턴 타입에는 사용할 수 있습니다. T타입의 프로퍼티도 val(읽기 전용) 또는 private var 로 선언할 수 있습니다.

GenClassA<in T>

GenClassA<in T> 에서 T타입의 객체는 GenClassA<in T>의 외부에서 입력하는것만 가능합니다. 즉 GenClassA<in T>에 선언된 메서드(멤버함수)의 매개변수에는 T 타입을 사용 가능하지만 메서드의 리턴 타입에는 사용할 수 없습니다. T타입의 프로퍼티도 메서드를 통해 변경하는 것은 가능하지만 외부에서 참조하는 것은 불가능합니다. private 선언만 가능합니다.(protected 선언도 불가능하다)

적용 범위

Declaration-site에서 in / out을 사용하면 Use-site에서 별도로 명시하지 않아도 in / out이 자동적으로 적용됩니다.


코드 중심의 외우기 쉬운 정리

G<out Tsuper>

공변

G<Sub>를 대입 가능

Tsuper는 출력만 가능


G<in Tsub>

반공변

G<Super>를 대입 가능

Tsub는 입력만 가능


+ Recent posts