오늘도 더 나은 코드를 작성하였습니까?

변성: 제네릭과 하위 타입 본문

Kotlin in Action/코틀린 답게 사용하기

변성: 제네릭과 하위 타입

hik14 2021. 3. 2. 18:04

변성의 개념은 List<String>과 List<Any>와 같이 기저 타입이 같고 타입 인자가 다른 여러 타입이 서로 어떤 관계가 있는지를 설명하는 개념이다.

 

변성이 있는 이유

 

List<Any> 타입의 파라미터를 받는 함수에 List<String>을 넘기면 안전할까?

자신있게 안정성을 보장할 수 없다.

 

fun printContents(list: List<Any>){
    println(list.joinToString())
}

fun main() {
    val list = listOf("abc", "efg")
    printContents(list)
}

위 경우는 잘작동 한다. 각 원소를 Any 타입으로 받아들이는데 String은 Any의 하위 타입이기 때문이다.

 

어떤 함수가 리스트의 원소를 추가하거나 변경한다면, 타입의 불일치가 생겨 불가능하지만, 원소의 추가나 변경이 없으면 가능하다.

 

클래스, 타입, 하위타입

 

클래스 = 타입 일까?

var x: String

var x: String? 

클래스 이름을 널이 될 수 있는 타입에도 사용하기에 최소한 코틀린에서 클래스에는 2개 이상의 타입을 구성할 수 있다.

 

제네릭 클래스는 더욱 복잡하다.

구체적인 타입을 얻기 위해서는 타입 파라미터를 구체적인 타입 인자로 치환해야된다.

 

List는 타입이 아닌 클래스다.

List<Int>,  List<String>,  List<Person>등 제네릭 클래스는 무수히 많은 타입을 생성할 수있다.

 

 

하위타입

 

타입 A의 값이 필요한 곳에 타입 B의 값을 넣어도 아무 문제가 없다면 B는 A의 하위타입이다. 

상위타입은 반대다.

한 타입이 다른 타입의 하위타입인지가 왜 중요할까? 

컴파일러는 변수 대입 및 함수로 인자 전달시 하위타입 검사를 매번 실행한다.

 

간단한 경우 하위 타입은 하위 클래스랑 같다. 

 

널이 될 수 없는 타입은 널이 될 수 있는 타입의 하위 타입이다. 하지만 두 타입 모두 동일한 클래스다. 

항상 널이 될 수 없는 타입의 값을 널이 될 수 있는 타입의 변수에 저장할 수는 있지만 반대로 널이 될 수 없는 타입에 널이 될수 있는 타입을 저장할 수없다.

 

타입 List<String>을   타입 List<Any>를 파라미터로 받는 함수에도 전달 해도 괜찮은가는

즉, List<String>이  List<Any>의 하위 타입인가?와 같다. 

 

제네릭 타입을 인스턴스화 할 때,

타입 인자로 서로 다른 타입이 들어가면 인스턴스 타입 사이의 하위 타입관계가 성립하지 않으면 그 제네릭 타입을 무공변성이라고 한다.

A가 B의 하위 타입이면, List<A>는 List<B>의 하위 타입이다. 이런 클래스나 인터페이스를 공변적이라 말한다. 

 

공변성: 하위 타입 관계를 유지한다.

cat은 Animal의 하위 타입이다, Producer<A>는  Producer<B> 하위 타입이다.

코틀린에서 제네릭 클래스 타입 파라미터에 대해 공변적임을 표시하려면 파라미터 앞에 out을 넣어야 한다.

interface Producer<out T>{
    fun produce(): T
}

 

 

 

타입 파라미터를 공변적으로 만들면 함수 정의에 사용한 파라미터 타입과 타입 인자가 정확히 일치하지 않더라도 그 클래스의 인스턴스를 함수 인자나 반환 값으로 사용가능하다. 

 

무공병선 역활을 하는 클래스 정의 하기.

open class Animal(val kind: String){

    fun feed() {
        println("${kind}가 먹이를 먹는다.")
    }
}

class Herd<T: Animal>(val animals: List<T>) {
    val size: Int
    get() = animals.size

    operator fun get(i: Int): T = animals[i]

}

fun feedAll(animals: Herd<Animal>){
    for (i in 0 until animals.size){
        animals[i].feed()
    }
}


fun main() {
    val animals = listOf(Animal("고양이"), Animal("개"), Animal("호랑이"))

    val herd = Herd(animals)
    feedAll(animals = herd)
}

 

무공변 컬렉션 역활을 하는 클래스 사용하기.

open class Animal(val kind: String){

    fun feed() {
        println("${kind}가 먹이를 먹는다.")
    }
}

class Cat(val name: String) : Animal("고양이"){
    fun cleanLitter(){
        println("$kind $name is cleaned")
    }
}

class Herd<T: Animal>(val animals: List<T>) {
    val size: Int
    get() = animals.size

    operator fun get(i: Int): T = animals[i]

}

fun feedAll(animals: Herd<Animal>){
    for (i in 0 until animals.size){
        animals[i].feed()
    }
}

fun takeCareOfCats(cats: Herd<Cat>){
    for (i in 0 until cats.size) {
        cats[i].cleanLitter()
        //feedAll(cats)  에러 inferred type is Hers<Cat> but Herd<Animal> was expected
    }
}

fun main() {
    val cats = listOf(Cat("나비"), Cat("두부"), Cat("유자"))
    val catHerd = Herd(cats)
    takeCareOfCats(cats = catHerd)
}

feedAll 은 사용이 불가능하다. 타입 불일치 로 인해서,

Herd 클래스의 T타입 파라미터에 대해 어떤 변성도 지정하지 않았기 때문에 고양이 무리는 동물 무리의 하위 타입이 아니다.

 

공변적 컬렉션 역활하는 하는 클래스로 변경

open class Animal(val kind: String){

    fun feed() {
        println("${kind}가 먹이를 먹는다.")
    }
}

class Cat(val name: String) : Animal("고양이"){
    fun cleanLitter(){
        println("$kind $name is cleaned")
    }
}

class Herd<out T: Animal>(val animals: List<T>) {
    val size: Int
    get() = animals.size

    operator fun get(i: Int): T = animals[i]

}

fun feedAll(animals: Herd<Animal>){
    for (i in 0 until animals.size){
        animals[i].feed()
    }
}

fun takeCareOfCats(cats: Herd<Cat>){
    for (i in 0 until cats.size) {
        cats[i].cleanLitter()
    }
    feedAll(catHerd) // 호출 가능하다.
}


fun main() {
    val cats = listOf(Cat("나비"), Cat("두부"), Cat("유자"))
    val catHerd = Herd(cats)

    takeCareOfCats(cats = catHerd)
  
}

모든 클래스를 공변적으로 할 수는 없다. 공변적으로 만들면 안전하지 못한 클래스도 있다.

타입 파라미터를 공변적으로 지정하면 클래스 내부에서 그 파라미터를 사용하는 방법을 제한한다. 

 

타입 안정성을 보장하기 위해 공변적 파라미터는 항상 OUT위치에 만 있어야된다.

클래스가 T 타입의 값을 생산할 수는 있지만, 타입의 값을 소비할수는 없다

 

클래스 맴버를 선언할 때 타입 파라미터를 사용할 수 있는 지점은

In - T가 함수의 파라미터 타입에 쓰인다.(소비한다.)

Out - T가 함수의 반환값에 쓰인다(생산한다.)

 

Herd클래스 타입 파라미터 T를 사용하는 장소는 오직 get의 반환 타입뿐이다.

class Herd<out T: Animal>(val animals: List<T>) {
    val size: Int
    get() = animals.size

    operator fun get(i: Int): T = animals[i]

}

Cat이 Animal의 하위 타입이라 Herd<Animal>의 get을 호출하는모든 코드는 Cat을 반환해도 아무문제 없이 작동한다. 

 

공변성:  하위 타입의 관계가 콜렉션 및 타입인자로 사용될때 관계가 유지된다.

사용제한: T를 반환 위치에서만 사용가능하다.