What is generic?
Generic이란 Class 또는 method에서 매개변수에 사용되는 자료형의 정의를 개체 생성시 정하게 하여 타입에 대한 안정성을 높이는 도구를 말한다. 일반적으로 <T>
로 표기된다.
Generic 사용의 장점 –
- Type casting is evitable- typecasting을 하지 않고 객체를 사용할 수 있다.
- Type safety- Generic allows only single type of object at a time.
- Compile time safety- 런타임 에러를 방지하기 위해 Generics code는 컴파일 타임에 체크된다.
이런 Generic에서 사용할 수 있는 타입의 범위를 지정하는것이 Type Bound이다. Modern Language들은 대부분 Type Bound 개념을 제공한다. Type Bound는 세 가지로 분류할 수 있다.
Type Bound (Parameter Variance) 분류 -
- 불변성(무공변성, invariant)
- 공변성(covariant)
- 반공변성(contravariant)
1. 불변성(무공변성, invariant)
상속 관계에 상관없이 자신의 타입만 허용하는 것을 뜻한다. Kotlin에서는 따로 지정해주지 않으면 기본적으로 모든 Generic Class는 무공변이다. Java에서의 <T>
와 같다.
open class Alcohol
class Soju : Alcohol()
interface Drinker<T> {
fun drink()
}
fun varianceTest(input: Drinker<Alcohol>){
input.drink()
}
fun main() {
val alchol: Drinker<Alcohol> = object:Drinker<Alcohol>{
override fun drink(){
println("Drink!")
}
}
val soju: Drinker<Soju> = object:Drinker<Soju>{
override fun drink(){
println("Drink Soju!")
}
}
// Success
println(varianceTest(alchol)) // Drink!
// Error
println(varianceTest(soju)) // Type mismatch: inferred type is Drinker<Soju> but Drinker<Alcohol> was expected
}
예시에서 Soju
는 Alcohol
을 상속받았지만, 별도로 공변성을 지정하지 않았으므로 무공변이다. 따라서 입력으로 Drinker<Alcohol>
타입을 받는 함수 varianceTest
에서 Drinker<Soju>
를 입력하면 Type mismatch 에러가 발생한다.
2. 공변성(covariant)
자기 자신과 자식 객체를 허용한다. Java에서의 <? extends T>
와 같다. Kotlin에서는 out
키워드를 사용해서 이를 표시한다.
open class Alcohol
class Soju : Alcohol()
interface Drinker<T> {
fun drink()
}
fun varianceTest(input: Drinker<out Alcohol>){ // out keyword 추가
input.drink()
}
fun main() {
val alchol: Drinker<Alcohol> = object:Drinker<Alcohol>{
override fun drink(){
println("Drink!")
}
}
val soju: Drinker<Soju> = object:Drinker<Soju>{
override fun drink(){
println("Drink Soju!")
}
}
val any: Drinker<Any> = object:Drinker<Any>{
override fun drink(){
println("Drink Any!")
}
}
// Success
println(varianceTest(alchol)) // Drink!
// Success
println(varianceTest(soju)) // Drink Soju!
// Error
println(varianceTest(any)) //Type mismatch: inferred type is Drinker<Any> but Drinker<out Alcohol> was expected
}
Soju
는 Alcohol
을 상속받아 구현한 하위 타입이다. varianceTest
함수에서 입력을 Drinker<out Alcohol>
로 지정하였으므로 Alcohol
과 Alcohol
의 하위 타입인 Soju
를 모두 입력으로 사용할 수 있다.
cf. Soju
는 Alcohol
의 하위 타입일 때 Drinker<Soju>
는 Drinker<Alcohol>
의 하위 타입이므로 Drinker
는 타입 인자 T에 대해 공변적이다.
cf. 공변/반공변에 대해 설명하기 위해 작성한 위 코드는 오직 개념 설명만을 위한 예시이다. 실제 코드를 작성할 때에는 in
, out
키워드에 대한 개념을 확실히 이해하는 것이 중요하다.
3. 반공변성(contravariant)
공변성의 반대 - 자기 자신과 부모 객체만 허용한다. Java에서의 <? super T>
와 같다. Kotlin에서는 in
키워드를 사용해서 표현한다.
open class Alcohol
class Soju : Alcohol()
interface Drinker<T> {
fun drink()
}
fun varianceTest(input: Drinker<in Alcohol>){ // in keyword 추가
input.drink()
}
fun main() {
val alchol: Drinker<Alcohol> = object:Drinker<Alcohol>{
override fun drink(){
println("Drink!")
}
}
val soju: Drinker<Soju> = object:Drinker<Soju>{
override fun drink(){
println("Drink Soju!")
}
}
val any: Drinker<Any> = object:Drinker<Any>{
override fun drink(){
println("Drink Any!")
}
}
// Success
println(varianceTest(alchol)) // Drink!
// Error
println(varianceTest(soju)) // Type mismatch: inferred type is Drinker<Soju> but Drinker<in Alcohol> was expected
// Success
println(varianceTest(any)) // Drink Any!
}
varianceTest
함수에서 입력을 Drinker<in Alcohol>
로 지정하였으므로 Alcohol
과 Alcohol
의 상위 타입(예시 코드에서는 별도로 지정된 상위 타입이 없음, Any
는 모든 객체의 상위 타입이므로 사용 가능)을 입력으로 사용할 수 있다.
open class Alcohol
class Soju : Alcohol()
interface Drinker<T> {
fun drink()
}
fun varianceTest(input: Drinker<in Soju>){ // in Soju로 변경
input.drink()
}
fun main() {
val alchol: Drinker<Alcohol> = object:Drinker<Alcohol>{
override fun drink(){
println("Drink!")
}
}
val soju: Drinker<Soju> = object:Drinker<Soju>{
override fun drink(){
println("Drink Soju!")
}
}
val any: Drinker<Any> = object:Drinker<Any>{
override fun drink(){
println("Drink Any!")
}
}
// Success
println(varianceTest(alchol)) // Drink!
// Success
println(varianceTest(soju)) // Drink Soju!
// Success
println(varianceTest(any)) // Drink Any!
}
varianceTest
함수에서 입력을 Drinker<in Soju>
로 지정하였으므로 Soju
와 Soju
의 상위 타입인 Alcohol
도 입력 가능하다.
How to use?
여기서 왜 out
과 in
이라는 키워드를 사용하는지 살펴보자면, 이는 Producer(생산자, read-only)와 Consumer(소비자, write-only)에 빗댄것이라는 것을 알 수 있다. - C#에서도 Kotlin과 같이 공변성과 반공변성을 out
, in
키워드를 사용한다. Kotlin이 이 개념을 따서 만든듯하다. Kotlin에 비해 C# 레퍼런스가 더 많으니 한번 찾아볼 만 하다.
In "clever words" they say that the class C is covariant in the parameter T, or that T is a covariant type parameter. You can think of C as being a producer of T's, and NOT a consumer of T's. In addition to out, Kotlin provides a complementary variance annotation: in. It makes a type parameter contravariant: it can only be consumed and never produced.
공식 홈페이지에서는 이렇게 설명하고 있다. - class C
가 type parameter T
에 대해 공변성(out
)을 가지면 C
는 T
의 소비자가 아닌 생산자로 볼 수 있다. 또한 T
가 반공변성(in
)을 가지면 그것은 소비만 할 수 있고 생산될 수는 없다.
out = Producer
// Java
interface Source<T> {
T nextT();
}
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!! Not allowed in Java
// ...
}
위 코드는 Java에서의 Generic 예시이다. Source<T>
는 T
를 parameter로 쓰지 않고 오직 리턴만 한다. 그러므로 Source<String>
인스턴스에 대한 레퍼런스를 Source<Object>
에 저장하는 것은 안전하다. 어차피 Consumer 호출이 없기 때문이다 .하지만 Java는 이를 알지 못하기 때문에 금지한다. 그래서 Source<? extends Object>
타입으로 선언해서 써야하는데, 이는 변수에 대해 동일한 method를 호출할 수 없으므로 큰 이득이 없고 타입만 더 복잡해진다. 또한 컴파일러가 이를 모른다.
abstract class Source<out T> {
abstract fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // This is OK, since T is an out-parameter
// ...
}
그래서 Kotlin은 이를 컴파일러에게 알려준다. 이 방식을 선언 위치 변성(declaration-site variance)이라고 부른다. Source
의 type parameter T
에 대해 Source<T>
의 멤버에서 T를 리턴만 하고 소비하지 않는다는 것을 명시한다. 이를 out
키워드로 표현한다.
그러므로 위에서 공변/반공변이 무엇인지 설명하면서 작성한 코드는 Java에서의 개념을 그대로 답습한 코틀린스럽지 못한 코드이다!!
in = Consumer
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
// Thus, we can assign x to a variable of type Comparable<Double>
val y: Comparable<Double> = x // OK!
}
위 예제코드는 반공변의 대표적인 예시인 compareTo
이다. Comparable
의 type parameter T
는 소비되기만 하고 생산되지는 않는다.
외우기 쉽게 정리하자면 class C에 대해
- class C가 type T를 생산 = class C가 type T를 리턴 = out
- class C가 type T를 소비 = class C가 type T를 파라미터로 사용 = in
Reference
'Language > Kotlin' 카테고리의 다른 글
[kotlin/mockk] mockStatic 해제하기 - clearStaticMockk (0) | 2022.01.26 |
---|---|
[kotlin/mockk] mockk로 LocalDatetime.now() mock 테스트 (0) | 2022.01.12 |
[Kotlin] Java Scripting API (JSR-223) (0) | 2019.10.19 |
Kotlin에서 Util 함수 작성하기 - Top-Level Functions (3) | 2019.10.14 |
Kotlin Infix Notation (중위 표기법) (0) | 2019.09.17 |
댓글