개발관련일지

코틀린 제네릭 학습정리 본문

개발기록/코틀린

코틀린 제네릭 학습정리

BEECHANGBOT 2023. 7. 30. 19:28

제네릭 관련 헤깔리는게 좀 있어서 아는 지식을 정리함

 

generic 사전적 의미는 포괄적인 , 통칭(총칭)의 라는 뜻을 가지고 있다. 

 

자바와 코틀린에서 타입을 지정하기 위해 사용한다.

타입 파라미터란 <T> 이거다. 구체적인 타입을 인자로 전달하기 위해서 사용한다.

간단한 예시로 제네릭이 없으면 리스트안에 인트고 스트링이고 다 들어가면 런타임중에 타입문제가 발생 할 수 있다.  그리고 해당 타입이 들어간다는걸 명시적으로 알 수 있어서 개발함에 있어서 타입관련해서 타입을 인식할 수 있다. 

 

사용법으론 fun <T> funcName() {} , class className<T> 등으로 사용할 수 있고 여러개도 가능하다. 가장 많이 접한는건 컬렉션 클래스들이지 않을까싶다. 꼭 하나만 사용하는게 아니라 여러개도 사용이 가능하다. 

public inline fun <T> List(size: Int, init: (index: Int) -> T): List<T> = MutableList(size, init)
public interface MutableList<E> : List<E>, MutableCollection<E> {
public interface Set<out E> : Collection<E> {
public interface Map<K, out V> {

타입파라미터는 <T : 상한타입(upper bound)를 지정할 때 객체 생성시 상한 타입이여야한다.

class NumberContainer<T : Number>(val number: T) {
    fun print() {
        println("The number is $number")
    }
}

메인함수 {
	val intContainer = NumberContainer(1) // T는 Int, 상한인 Number의 하위 타입
}

제네릭은 타입소거(Type Erasure)를 하므로 JVM에서 제네릭은 런타임에 타입에 대한 정보가 없다. -> 타입이 확인 불가능, 문자열일 경우 그냥 문자열들의 메모리주소만 알고 있는거다.

// 1번 
fun <T> sampleFunc(input: T) {
    if (input is T) {
        //e: file:경로/ko.kt:8:1 Platform declaration clash: 
        //The following declarations have the same JVM signature (genericFunction(Ljava/lang/Object;)V):
        //    fun <T> genericFunction(input: T): Unit defined in 패키지명
        //    inline fun <reified T> genericFunction(input: Any): Unit defined in 패키지명
     }
}

//2번 
//reified을 사용하면 가능해지고 reified은 인라인을 붙여야지 사용 할 수있다.
inline fun <reified T> sampleFunc(input: Any) {
    if (input is T) {
        // do something
    }
}

그래서 런타임 중에 제네릭의 대한 타입을 추론하는게 불가능하고 1번과 같은 경우 빌드하면 에러발생한다. 

타입을 확인해야 할 경우 reified를 타입파라미터에 추가 후 사용하면된다. 대신 사용하는 함수는 inline 함수로 만들어줘야한다.

인라인 함수는 넘어오는 파라미터값을 함수 내로 추가 시키는 특성이 있다. 인라인을 사용하면 컴파일 단계에서 2번 기준으로 input이 함수안으로 추가되게되고 타입추론이 가능해 지는걸로 보인다. 

reifed안쓸거면 그냥 직접 타입을 넘겨주면되고 의문이드는게 이럴꺼면 그냥 inline 만 사용하게 해놓았어도 되지않을까 생각이들고 reifed의 기능적인 역할은 찾지 못했다. 명시적으로만 나타내기 위함인지는 모른다.

 

변성 : 제네릭 타입에서 하위타입 관계를 정의하는것을 말한다.

호출부에서 위험 여지가 있는 경우를 방지하기위해 메서드들이 안전하게 호출 할 수 있게 사전에 문제를 컴파일 타임에서 잡기위함, 생성자는 인스턴스 직후에 바로 동작하기 떄문에 영향을 받지않는다

A타입이 사용되는 모든 곳에 B타입이 사용가능하면 A는 B의 상위타입이고, B는 A의 하위타입이다(클래스 부모자식관계를 말하는게 아님 역할로 써는 그렇지만 변성의 관계에서는 무조건 부모클래스가 자식클래스의 하위타입이 되는게아님)

 

무공변 : 제네릭 타입을 인스턴스화 할시 타입인자로 다른 타입이 들어가면 해당 객체들 사이에서는 하위타입 관계가 성립하지않음(자바는 원래 무공변) , 부모자식클래스라고 무조건 하위타입이 되는게아니다.

open class Animal {
    fun feed() {}
}

class Cat : Animal() {
    fun meow() { }
}

// MutableList는 무공변(Invariant)
fun handleAnimals(animals: MutableList<Animal>) {}

// 컴파일 에러! MutableList<Cat>은 MutableList<Animal>의 하위 타입이 아님
val cats = mutableListOf(Cat(), Cat(), Cat())
handleAnimals(cats)
/*
    Type mismatch.
    Required:
    MutableList<Animal>
    Found:
    MutableList<Cat>
*/

공변 : 하위타입관계를 유지시켜준다 <out T>

반공변 : 상위타입 관계를 유지시켜준다 <in T>

class InAndOut<out T, in R>(private val t: T) {
    fun sample(input : R/*IN*/) : T/*OUT*/{
        return t
    }
}

공변(out)은 out위치 에서만 사용 할 수 있고, 반공변은(in)은 생성 위치에서만 사용할 수 있다.

 

무공변선 공변성 반공변성
<T> out T in T
하위 타입 관계 X 하위타입관계유지 하입타입관계 뒤집힘
X Cat은 Animal의 하위타입 Animal은 Cat의 하위타입
T는 어디나 사용가능 T는 아웃위치만 사용가능 T는 인위치만 사용가능

공변성, 반공변성

open class Animal {
    fun feed() {}
}

class Cat : Animal() {
    fun meow() {}
}

// 공변성 - `out` 키워드 사용
class AnimalHouse<out T : Animal>(val animal: T) {
    fun getAnimal(): T {
        return animal
    }
}

fun main() {
    val catHouse: AnimalHouse<Animal> = AnimalHouse(Cat()) // 공변성 사용
    val cat: Animal = catHouse.getAnimal()
    
    //out 없애면 Type mismatch. Required:MutableList<Animal> , Found: MutableList<Cat>
}

 

스타프로텍션 <*> : 특정타입에 대한 정보가 없을 시 사용한다. 모든 타입을 받을 수 있는데 한번 타입이 정해지면 해당 타입만 가능하다.

구체적인 값을 받기 전까진 Any? 이다. 

반공변 + 스타프로텍션을 사용하면 아무것도 못들어온다. val list = List<in Noting> =  listof()

 

참고 

코틀린인액션