ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 코틀린 고급편 - 제네릭
    Kotlin/코틀린 고급편 2023. 12. 19. 15:52

    1. 제네릭과 타입 파라미터

    fun main() {
        /**
         * example01
         */
        val cage = Cage()
        cage.put(Carp("잉어"))
    //    val carp: Carp = cage.getFirst()              // Error: Type Mismatch
    //    val carp: Carp = cage.getFirst() as Carp      // 만약에 넣은게 금붕어였다면?! -> 런타임 에러
        
        // Safe Type Casting과 Elvis Operator
        val carp: Carp = cage.getFirst() as? Carp?: throw  IllegalArgumentException()
        
        
        // generic 활용
        val cage2 = Cage2<Carp>()
        cage2.put(Carp("잉어"))
        val carp2: Carp = cage2.getFirst()
    }
    class Cage {
        private val animals: MutableList<Animal> = mutableListOf()
    
        fun getFirst(): Animal {
            return animals.first()
        }
    
        fun put(animal: Animal) {
            this.animals.add(animal)
        }
    
        fun moveFrom(cage: Cage) {
            this.animals.addAll(cage.animals)
        }
    }
    
    class Cage2<T> {
        private val animals: MutableList<T> = mutableListOf()
    
        fun getFirst(): T {
            return animals.first()
        }
    
        fun put(animal: T) {
            this.animals.add(animal)
        }
    
        fun moveFrom(cage: Cage2<T>) {
            this.animals.addAll(cage.animals)
        }
    }

     

     

    2. 배열과 리스트, 제네릭과 무공변

    - 상속관계의 의미 : 상위 타입이 들어가는 자리에 하위 타입이 대신 위치할 수 있다.

    fun main() {
        /**
         * example02
         */
        val goldFishCage = Cage2<GoldFish>()
        goldFishCage.put(GoldFish("금붕어"))
    
        val fishCage = Cage2<Fish>()
    //    fishCage.moveFrom(goldFishCage) // Type Mismatch
    }

        ㆍCage2<Fish>와 Cage2<GoldFish>는 아무 관계도 없다 -> 무공변(불공변)하다

     

    - Java의 배열

        ㆍObject는 String의 상위타입이라면, Object[]는 String[]의 상위 타입으로 간주된다.

        ㆍJava의 배열은 공변하다.

    String[] strs = new String[]{"A", "B", "C"};
    Object[] objs = strs;
    objs[0] = 1;	// ArrayStoreException

        ㆍobjs는 사실 String[] 이기 때문에 int를 넣을 수 없다. 때문에 런타임 에러 발생!

        ㆍ타입 안전하지 않는 위험한 코드이다.

    - Java의 리스트

        ㆍList는 제네릭을 사용하기 때문에 공변인 Array와 다르게 무공변하다.

    List<String> strs = List.of("A", "B", "C");
    List<Object> objs = strs;	// Type Mismatch

        ㆍ컴파일 에러 발생

     

    3. 공변과 반공변

    fun moveFrom(cage: Cage2<out T>) {
        this.animals.addAll(cage.animals)
    }

        ㆍout을 붙이면 moveFrom 함수를 호출할 때 Cage2는 공변하게 된다.

        ㆍout을 통해 변성(variance)를 주었기 때문에 out을 variance annotation 이라고 부른다.

     

    - out을 붙이게 되면, otherCage로부터 데이터를 꺼낼 수만 있다!

    fun moveFrom(otherCage: Cage2<out T>) {
        otherCage.getFirst()
    //        otherCage.put("test") // error! because out
        this.animals.addAll(otherCage.animals)
    }

        ㆍotherCage는 생산자(데이터를 꺼내는) 역할만 할 수 있다고 한다.

     

    - 왜 out을 붙이면 파라미터가 생산자 역할만 할 수 있을까?

        ㆍ타입안전성을 위해!

     

     

    fun moveTo(otherCage: Cage2<T>) {
        otherCage.animals.addAll(this.animals);
    }
    /**
     * example03
     */
    val fishCage2 = Cage2<Fish>()
    
    val goldFishCage2 = Cage2<GoldFish>()
    goldFishCage2.put(GoldFish("금붕어"))
    goldFishCage2.moveTo(fishCage2)     // Type Mismatch - Cage2<Fish>가 Cage2<GoldFish>의 하위 타입이어야 한다.
                                        // Cage2를 moveTo 함수에서 반공변하게 만들어야한다!

    - in 키워드 사용!

    fun moveTo(otherCage: Cage2<in T>) {
        otherCage.animals.addAll(this.animals);
    }

        ㆍin이 붙은 otherCage는 데이터를 받을 수만 있다!

        ㆍotherCage는 '소비자'이다!

     

    정리

    - out : (함수 파라미터 입장에서의) 생산자, 공변

        ㆍ공변 : 타입 파라미터의 상속 관계가 제네릭 클래스에서 유지된다.

    - in : (함수 파라미터 입장에서의)  소비자, 반공변

        ㆍ반공변 : 타입 파라미터의 상속 관계가 제네릭 클래스에서 반대로 된다.

     

    4. 선언 지점 변성 / 사용 지점 변성 

    - 상위 타입 변수에 하위 타입 변수 넣기

    val int: Int = 1_000
    val num: Number = int

     

    - out을 사용해 상위 타입 변수에 하위 타입 변수 넣기

    val goldFishCage: Cage2<GoldFish> = Cage2()
    val fishCage: Cage2<out Fish> = goldFishCage

     

    - Java에 있는 와일드 카드 타입과 대응된다.

    List<Integer> ints = List.of(1,2,3);
    List<? extends Number> nums = ints;

     

    * <out T> = <? extends T>

    * <in T> = <? super T>

     

    - 제네릭 클래스 자체를 공변하게 만들 수 없을까?

    class Cage3<out T> {
        private val animals: MutableList<T> = mutableListOf()
    
        fun getFirst(): T {
            return this.animals.first()
        }
    
        fun getAll(): List<T> {
            return this.animals
        }
    }

        ㆍ해당 클래스는 오직 생산만 하고 있다.

        ㆍ이런 경우 클래스 자체를 공변하게 만들 수 있다! (배열에 값을 넣지 않기 때문에!)

        ㆍ또한, out을 붙였기에 생산만 가능하고, 소비를 위한 T를 받을 수는 없다!

     

    - 소비만 하는 클래스

    class Cage4<in T> {
        private val animals: MutableList<T> = mutableListOf()
    
        fun put(animal: T) {
            this.animals.add(animal)
        }
    
        fun putAll(animals: List<T>) {
            this.animals.addAll(animals)
        }
    }

     

    * 변성을 주는 위치에 따라 클래스 자체에 out을 붙일 수 있고, 함수에만 붙일 수도 있다.

     

        ㆍ클래스 선언에 변성을 준다 - declaration-site variance

        ㆍ함수나 변수 지점에 변성을 준다 - use-site variance

     

    * @UnsageVariance

    - 코틀린에서의 List를 보면 out이 붙어있다.

        ㆍout class에서 생산만 가능하지만, 정말 타입안전하다고 생각되는 소비할 수 있는 타입 파라미터 앞에 붙이면 에러가 안난다.

        ㆍ원래는 out 선언지점변성을 활용해 E를 함수 파라미터에 쓸 수 없지만, @UnsafeVariance를 이용해 함수 파라미터에 사용했다.

     

     

    5. 제네릭 제약과 제네릭 함수

    class Cage5<T : Animal> {
        private val animals: MutableList<T> = mutableListOf()
    }

        ㆍT: Animal 을 사용하면, 타입 파라미터의 상한을 Animal로 정할 수 있다.

     

    class Cage5<T> (
        private val animals: MutableList<T> = mutableListOf()
    ) where T : Animal, T : Comparable<T>

        ㆍwhere 키워드를 사용해 타입 파라미터에 여러 조건을 설정할 수 있다.

        ㆍ이런 경우, Cage 클래스 안에 있는 동물들을 순서대로 정렬해 출력하는 함수를 만들 수 있다.

     

    - 제네릭 제약을 Non-Null 타입 한정에 사용할 수도 있다.

    class Cage2<T : Any> {
    }
    
    fun main() {
        val nullableCage = Cage2<Animal?>()	// complie error
    }

     

     

    6. 타입 소거와 Star Projection

    - 코틀린도 JVM 위에서 동작하기 때문에 런타임 때는 타입 정보가 사라진다.

        ㆍ이를 어려운 말로 타입 소거라 부른다.

     

    fun checkStringList(data: Any) {
        // Error : Cannot check for instance of erased type: List<String>
    //    if (data is List<String>) {
    //    }
        
        // data가 List인지 확인할 수 있다.
        if (data is List<*>) {  // star projection을 활용해 최소한 List인지는 확인할 수 있다.
        	val element: Any? = data[0]
        }
    }

    - star projection : 해당 타입 파라미터에 어떤 타입이 들어 있을지 모른다는 의미

     

    inline fun <reified T> List<*>.hasAnyInstanceOf(): Boolean {
        return this.any { it is T }
    }

    - inline 함수 : 코드의 본문을 호출 지점으로 이동시켜 컴파일되는 함수

    - refined 키워드의 한계

        ㆍrefined 키워드가 붙은 타입 T를 이용해 T의 인스턴스를 만들거나 / T의 companion object를 가져올 수는 없다.

     

    7. 제네릭 용어 정리 및 간단한 팁

    - 제네릭 클래스

    class Cage2<T> {

        ㆍ타입 파라미터를 사용한 클래스

     

    - Raw 타입

    List list = new ArrayList();

        ㆍ제네릭 클래스에서 타입 매개변수를 사용하지 않고 인스턴스화 하는 것

        ㆍ코틀린에서는 Raw 타입 사용이 불가능하다.

     

    - 변성

        ㆍ제니릭 클래스 타입 파라미터에 따라 제네릭 클래스간의 상속 관계가 어떻게 되는지를 나타내는 용어

     

    - 무공변(불공변, in-variant)

        ㆍ타입 파라미터끼리는 상속관계이더라도, 제네릭 클래스 간에는 상속관계가 없다는 의미

        ㆍ변성을 부여하지 않았다면 제네릭 클래스는 기본적으로 무공변하다.

    - 공변(co-variant)

        ㆍ타입 파라미터간의 상속관계가 제네릭 클래스에도 동일하게 유지된다는 의미

        ㆍ코틀린에서는 out 변성 어노테이션을 사용하면 특정지점에서 공변하게 만들 수 있다.

    - 반공변(contra-variant)

        ㆍ타입 파라미터간의 상속관계가 제네릭 클래스에서는 반대로 유지된다는 의미

        ㆍ코틀린에서는 in 변성 어노테이션을 사용하면 특정지점에서 반공변하게 만들 수 있다.

     

    - 선언 지점 변성

    class Cage3<out T> {

        ㆍ클래스 자체를 공변하거나 반공변하게 만드는 방법

    - 사용지점 변성

    fun moveFrom(otherCage: Cage2<out T> {

        ㆍ특정 함수 또는 특정 변수에 대해 공변/반공변을 만드는 방법

     

    - 제네릭 제약

    class Cage5<T : Animal> {

        ㆍ제네릭 클래스의 타입 파라미터에 제한을 거는 방법

     

    - 타입 소거

        ㆍJDK의 호환성을 위해 런타임 때 제네릭 클래스의 타입 파라미터 정보가 지워지는 것

    inline fun <refined T> List<*>.hasAnyInstanceOf(): Boolean {

        ㆍkotlin에서는 inline 함수 + refined 키워드를 이용해 타입 소거를 일부 막을 수 있다.

     

    - Star Projection

    fun checkList(data: Any) {
        if (data is List<*>) {
            val element: Any? = data[0]
        }
    }

        ㆍ어떤 타입이건 들어갈 수 있다는 의미

     

     

    제네릭과 관련한 3가지 내용

    - 타입 파라미터 새도잉

    class Cage<T : Animal> {
        fun <T : Animal> addAnimal(animal: T) {
        }
    }

        ㆍ똑같은 T문자이지만, 클래스에서의 T와 함수에서의 T가 다른 것으로 간주된다.

        ㆍ클래스의 T가 함수의 T에 의해 shadowing 되는 것!

        ㆍ타입 파라미터 새도잉은 피해야 하고, ,만약 함수 타입 파라미터를 쓰고 싶다면 겹치지 않도록 해야한다.

     

    - 제네릭 클래스의 상속

    open class CageV1<T : Animal> {
        open fun addAnimal(animal: T) {
    
        }
    }
    
    class CageV2<T : Animal> : CageV1<T>()
    
    class GoldFishCageV2 : CageV1<GoldFish>() {
        override fun addAnimal(animal: GoldFish) {
            super.addAnimal(animal)
        }
    }

        ㆍ1. 하위 클래스를 만들 때 같은 타입 파라미터를 사용한다.

                - CageV1에 타입 파라미터를 넣어야 하므로 제네릭 제약 조건이 CageV2에도 전파

        ㆍ2. 아예 특정 파라미터를 정한다.

                - 오버라이드시 자동으로 타입을 명시적으로 표현

     

    - 제네릭과 Type Alias

        ㆍType Alias는 타입에 '별명'을 붙일 수 있게 해준다.

    fun handleCacheStore(store: Map<PersonDtoKey, MutableList<PersonDto>>) {
    }
    
    typealias PersonDtoStore = Map<PersonDtoKey, MutableList<PersonDto>>
    
    fun handleCacheStore(store: PersonDtoStore) {
    }

    댓글

Designed by Tistory.