[HeadFirst] Kotlin Generic

timothy jeong·2021년 11월 2일
0

코틀린

목록 보기
15/20

Generic

제네릭을 이용하는 것은 런타임 오류를 컴파일 오류로 바꿀 수 있는 좋은 방법이다. 아래와 같이 Generic 을 이용하여 Collection 을 정의하면, String 이 아닌 타입의 원소가 추가될때 컴파일러가 오류를 인지할 것이다. 만약 Generic 으로 타입을 정의하지 않은 arrList2 의 케이스를 보면, 내부적으로 arrList2 는 Any 타입으로 정의 되어 있을 것이다.

fun main() {    
    val arrList: MutableList<String> = mutableListOf("str1", "str2", "str3")
    arrList.add(Recipe("Soup")) // Type mismatch: inferred type is Recipe but String was expected
    
    val arrList2 = mutableListOf(Recipe("Soup"), Recipe("KimChi"), Animal("Hippo"))
    
    for (x in arrList) {
        x.sayTitle() // Unresolved reference: sayTitle
    }
}

data class Recipe (val title: String) {
    fun sayTitle() {
        println("this is $title recipe")
    }
}

class Animal (val name: String) {
    fun sayName() {
        println("$name is Animal")
    }
}

위에 예시로 들은 MutableList 가 어떤식으로 정의되어 있는지 보자, Generic 이 들어갈 자리에 E 라는 문자가 들어가 있고, 그 E 를 하위 메서드에서도 그대로 사용하는 모습이다. 그리고 List 인터페이스와 MutableCollection 을 상속하고 있다.

E 는 개발자가 MutableList 를 정의할때 사용할 실제 타입을 의미하고, 이 자료형이 add 메서드에까지 이어지므로, 개발자가 정의한 타입이 아니면 Type mismatch 오류가 나오는 것이다.

public interface MutableList<E> : List<E>, MutableCollection<E> {
    override fun add(element: E): Boolean
    //More code
}

그리고 이상을 보면 MutableList 는 Class 가 아닌 인터페이스이다. 우리가 mutableListOf() 함수를 사용하면 MutableList 인터페이스를 구현해서 반환하는 것이다. 그리고 이렇게 구현된 인터페이스에는 필요한 모든 함수와, 프로퍼티가 포함되어 있다.

public fun <T> mutableListOf(vararg elements: T): MutableList<T> =
    if (elements.size == 0) ArrayList() else ArrayList(ArrayAsCollection(elements, isVarargs = true))

Generic 클래스, 인터페이스 만들기

Generic 임을 명시하는 문자에 대한 컨벤션이 있다.
일반적으로 'T'(for Type) 를, Collection 을 사용할때는 'E'(for Element) 를, Map 에 대해서는 'K'(Key) 와 'V'(Value) 를 이용한다.

이때, T : {someType} 을 이용해서 제네릭이 받을 수 있는 타입을 제한할 수 있다.

abstract class Pet (val name: String)

class Cat(name: String): Pet(name)
class Dog(name: String): Pet(name)
class Fish(name: String): Pet(name)

class Contest<T: Pet> {
    private val scores: MutableMap<T, Int> = mutableMapOf()
    fun addScore(t: T, score: Int = 0) {
        if (score < 0) 
            return
        if (scores[t] == null) {
            scores[t] = score
        } else {
            scores[t] = scores.getValue(t)  + score
        }
    }

    fun getWinner(): MutableSet<T> {
        val highScore = scores.values.maxOrNull()
        if (highScore == null) {
            println("no scores")
            return mutableSetOf()
        }
        val winners: MutableSet<T> = mutableSetOf()
        for ((t, score) in scores) {
            if (score == highScore) winners.add(t)
        }
        return winners
    }
}

fun main() {
    val catContest = Contest<Cat>()
    catContest.addScore(Cat("Ggomi"), 50)
    catContest.addScore(Cat("Nabi"), 45)
    val topCat = catContest.getWinner().first()
    println(topCat.name)

}

null 도 담을 수 있는 generic

class M () {
    fun <T> myFun() : T? {
        return null
    }
}

Generic 과 다형성(polymorphism)

interface Retailer<T:Pet> {
    fun sell(): T
}

class CatRetailer : Retailer<Cat> {
    override fun sell(): Cat {
        println("Sell Cat")
        return Cat("")
    }
}

class DogRetailer : Retailer<Dog> {
    override fun sell(): Dog {
        println("Sell Dog")
        return Dog("")
    }
}

fun main() {
    val catRetailer1 = CatRetailer()

    val petRetailer: Retailer<Pet> = CatRetailer() // this won't Compile, What about polymorphism?

}

위 예시에서 val petRetailer: Retailer<Pet> = CatRetailer() 이 당연히 작동해야 할것 같은데, 컴파일 되지 않는다. Retailer<Cat>, Retailer<Dog> 도 안되고, 오로지 Retailer<Pet> 만 가능하다. Generic 을 쓰면 다형성을 구현할 수 없는 걸까?

out 키워드

Generic supertype 이 subType 을 받을 수 있게 하기 위해서는 out 키워드를 앞에 써줘야한다. 위의 인터페이스에 out 키워드를 넣으면 컴파일 에러없이 프로그램이 잘 돌아간다.
out 키워드를 사용하면 해당하는 Generic 을 covariant 로 만든다. covariant 가 되면, supertype 자리에 subtype 을 넣을 수 있게 된다.

interface Retailer<out T:Pet> {
    fun sell(): T
}

......

fun main() {
    val catRetailer = CatRetailer()

    val petRetailer: Retailer<Pet> = CatRetailer() 
}

일반적으로 out 키워드는 Generic 을 return 하는 함수가 있는 class, interface 에서 사용하거나, Generic 타입의 val 변수가 있는 경우에 사용된다. 하지만 class, interface 가 Generic 을 함수 파라미터로 갖거나 var 프로퍼티의 타입으로 갖는 경우에는 사용할 수 없다.

여기서 왜 키워드가 out 인지 알 수 있다. out 키워드는 Generic 타입이 out 인 경우 (함수의 리턴타입, val는 read_only) 에만 사용할 수 있다.

in 키워드

수의사 클래스를 만들고 contest 에 프로퍼티로 넣자.

class Vet<T: Pet> {
    fun treat(t: T) {
        println("Threat Pet ${t.name}")
    }
}

class Contest<T: Pet> (var vet: Vet<T>) { ... }

모든 pet 을 치료할 수 있는 수의사를 콘테스트에 배치하면 정말 좋지 않을까? 그런데 Type mismatch 컴파일 에러가 나온다. 상속체계상 Pet 하위에 Cat 이 있는 상황이다.그러면 이번에도 out 키워드가 필요한 걸까? 하지만 var 를 이용하는 Generic 타입의 프로퍼티가 있는 경우 out 키워드를 사용할 수 없다.

fun main() {
    val petVet = Vet<Pet>()
    val catVet = Vet<Cat>()
    val dogVet = Vet<Dog>()
    
    val catContest2 = Contest<Cat>(petVet) // Type mismatch
}

우리의 상황은 var Vet<Pet>Contest<Cat> 의 프로퍼티로 만들고 싶은 것이다. 다른 말로 하자면, 우리는 Generic supertype 을 subtype 자리에 사용하고 싶은것이다. out 과는 완전 반대의 상황이다. 이때 Vet 클래스의 Generic 자리에 in 키워드를 이용하면 문제를 해결할 수 있다. in 키워드를 이용하면 Generic 의 type 을 contravariant 로 바꿔놓는다. ( out 은 covariant 이다 )

 class Vet<in T: Pet> {
    fun treat(t: T) {
        println("Threat Pet ${t.name}")
    }
} 

fun main() {
    val petVet = Vet<Pet>()
    val catVet = Vet<Cat>()
    val dogVet = Vet<Dog>()
    
    val catContest2 = Contest<Cat>(petVet) // Type mismatch
}

보통 Generic 타입의 인자를 받는 함수가 있는 경우 in 이 사용된다. 하지만 out 이 유효한 경우에는 in 을 사용할 수 없다.

in 을 지역적(locally) 으로 사용하기

in 이 sueprtype 을 subtype 자리에 넣을 수 있도록 바꿔주는 건 알겠다. 하지만 모든 상황에서? 몇몇 상황에서만 그렇게 할 순 없을까?

class 나 interface 에 in 키워드를 사용하는 것은 global 하게 사용하게 된다. 하지만 local 로 사용하면 Contest<Cat>Vet<T> 이 넘겨질 때에만 Vet<Pet>Vet<Cat> 대신 사용될 수 있도록 할 수 있다. Vet 클래스에서 in 을 삭제하고, Contest 의 생성자에 in 을 추가하면 된다.

class Vet<T: Pet> { ... }
class Contest<T: Pet> (var vet: Vet<in T>) { ... }

Vet 클래스처럼 Generic type 이 in 이나 out 키워드가 모두 없는 경우 invariant 하다고 한다.

profile
개발자

0개의 댓글