ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • kotlin) 제네릭
    kotlin 2024. 7. 21. 15:35

    제네릭과 타입 파라미터

    SafeType Casting 과 Elvis Operator

    추상 클래스 상속과 타입변환의 문제점

    fun main() {
      val cage = Cage()
      cage.put(Carp("잉어"))
    //  val carp: Carp = cage.getFirst()        //cage 에 잉어만 있지만, getFirst() 메소드를 호출하면 Animal 이 나온다.
      val carp : Carp = cage.getFirst() as Crap  //Carp 으로 단순 형변환을 하면 되지만 이러면 위험한 코드가 되어버린다.
    
      cage.put(GoldFish("금붕어"))
      val goldFish : Carp = cage.getFirst() as Carp //만약에 cage 안에 넣은게 Carp 이 아닌 GoldFish 라면?
    }
    
    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) {          // 다른 Cage 에 있는 동물을 모두 가져온다.
        this.animals.addAll(cage.animals)
      }
    }

     

    상속받은 클래스를 안전한 타입으로 가져오는 방법

    fun main() {
      val cage = Cage()
      cage.put(Carp("잉어"))
      //조금 더 안전하지만 결국 Exception 이 발생
      val carp: Carp? = cage.getFirst() as? Carp? : throw IllegalArgumentException()  
    
      //동일한 Cage 클래스이지만 잉어만 넣을 수 있는 Cage, 금붕어만 넣을 수 있는 Cage 를 구분하면 된다.
      val cage2 = Cage2<Carp>()
      val carp2 : Carp = cage2.getFirst()   //타입 미스 에러가 발생하지 않음
    }
    
    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>) {          // 다른 Cage 에 있는 동물을 모두 가져온다.
        this.animals.addAll(cage.animals)
      }
    }

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

    cage<T> 간의 함수 호출

    fun main() {
      
      val goldFishCage = Cage<GoldFish>()
      goldFishCage.put(GoldFish("금붕어"))
    
      val fishCage = Cage<Fish>()
      fishCage.moveFrom(goldFishCage)       //타입 미스 에러 발생
      fishCage.put(GoldFish("금붕어")) //하지만 GoldFish 는 넣을 수 있다.
    }
    class Cage<T> {
      private val animals: MutableList<T> = mutableListOf()
    
      fun getFirst(): T {            //첫번째 동물을 가져 온다
        return animals.first()
      }
    
      fun put(animal: T) {           //동물을 넣는다.
        this.animals.add(animal)
      }
    
      fun moveFrom(cage: Cage<T>) {          // 다른 Cage 에 있는 동물을 모두 가져온다.
        this.animals.addAll(cage.animals)
      }
    }
    • Cage<Fish> 와 Cage<GoldFish> 는 아무 관계도 아니다.
    • Cage 는 무공변(in-variant,불공변) 하다.
    • 왜 Fish와 GoldFish 간의 상속관계가 제네릭 클래스에서 유지되지 않을까?
    //Java
    String[] strs = new String[]{"A","B","C"};
    Object[] objs = strs;
    Objs[0] = 1;			//타입이 다르더라도 삽입이 가능하다. 하지만 런타임때 잡힌다.
    
    List<String> strs = List.of("A","B","C");
    List<Object> objs = strs;		//타입 미스가 발생

    공변과 반공변

    공변(Covariance)

    • 공변은 "같은 방향으로 변한다"는 뜻, 타입 A가 타입 B의 서브타입이면, Generic<A>도 Generic<B>의 서브타입이라는 의미
    • 함수를 공변하도록 만들면, 함수를 호출할 때 상위 타입이 되면서 넣을 수 있게 된다.
    fun main() {
    
      val goldFishCage = Cage<GoldFish>()
      goldFishCage.put(GoldFish("금붕어"))
    
      val fishCage = Cage<Fish>()
      fishCage.moveFrom(goldFishCage)       //out 을 넣으면 상속관계가 제네릭 클래스까지 이어져 넣을 수 있다.
      
      val cage: Cage<out Fish> = Cage<GoldFish>() // 선언시 넣으면 바로 공변,반공변하게 만들 수 있다.
    }
    class Cage<T> {
      private val animals: MutableList<T> = mutableListOf()
    
      fun getFirst(): T {            //첫번째 동물을 가져 온다
        return animals.first()
      }
    
      fun put(animal: T) {           //동물을 넣는다.
        this.animals.add(animal)
      }
    
      fun moveFrom(cage: Cage<out T>) {          // out 을 이용하면 공변하게 만들어준다.
        this.animals.addAll(cage.animals)
      }
    }

     

     

    out 을 사용하는 이유

    - out 키워드는 클래스나 인터페이스가 특정 타입의 값을 반환(출력)하기만 하고, 그 타입의 값을 설정(입력)하지 않는 경우에 사용됩니다. 이는 공변성을 통해 더 유연한 타입 시스템을 만들고, 타입 안전성을 유지하는 데 도움이 됩니다.

     

    fun moveFrom(cage: Cage<out T>) {          // out 을 이용하면 공변하게 만들어준다.
      cage.getFirst()
      cage.put(Carp("잉어"))    //에러가 발생함
      this.animals.addAll(cage.animals)
    }

     

     

    반공변(Contra-variant)

    • 반공변(Contravariance)은 제네릭 타입 파라미터를 다룰 때 타입 계층 구조에서 "반대 방향으로 변한다"는 개념을 설명합니다.
    • 즉, 타입 A가 타입 B의 서브타입일 때, Generic<B>가 Generic<A>의 서브타입이 되는 것을 의미합니다.
    • 이는 주로 함수 매개변수 위치에서 사용됩니다. 코틀린에서는 in 키워드를 사용하여 반공변을 표현할 수 있습니다.

    반공변의 개념

    반공변은 주로 메서드의 매개변수에서 사용됩니다. 반공변을 통해, 특정 타입의 슈퍼타입을 허용하여 더 일반적인 타입을 받을 수 있도록 합니다.

     

    fun main() {
    
      val fishCage = Cage<Fish>()
    
      val goldFishCage = Cage<GoldFish>()
      goldFishCage.put(GoldFish("금붕어"))
      goldFishCage.moveTo(fishCage)				//에러가 발생하지 않는다.
    }
    class Cage<T> {
      private val animals: MutableList<T> = mutableListOf()
    
      fun getFirst(): T {            //첫번째 동물을 가져 온다
        return animals.first()
      }
    
      fun put(animal: T) {           //동물을 넣는다.
        this.animals.add(animal)
      }
    
      fun moveTo(cage: Cage<in T>){				//in 을 넣으면 
        cage.animals.addAll(this.animals)
      }
    }

    제네릭 제약과 제네릭 함수

    타입 파라미터의 제한 조건

    fun main() {
      Cage<Int>()
      Cage<String>()
      Cage<Fish>()      //Animal 하위 타입만 가능하다
    }
    class Cage<T : Animal> {		//Animal 하위 타입만 가능하게 제네릭을 설정
      private val animals: MutableList<T> = mutableListOf()
    }

     

    제한 조건 여러개 설정 (ex. Animal 만 들어오고 Comparable 을 구현)

    class Cage<T> (
      private val animals: MutableList<T> = mutableListOf()		//주 생성자 설정
    )
      where T: Animal, T: Comparable<T> { 		//where 문을 이용해서 여러 제한 조건이 가능
    }

    응용

    class Cage5<T>(
      private val animals: MutableList<T> = mutableListOf()
    ) where T : Animal, T : Comparable<T> {
      fun printAfterSorting() {
        this.animals.sorted()
          .map { it.name }
          .let(::println)
      }
    }
    
    abstract class Bird(
      name: String,
      private val size: Int,
    ) : Animal(name), Comparable<Bird> {
    // 사이즈가 작은 새가 앞으로 온다.
      override fun compareTo(other: Bird): Int {
        return this.size.compareTo(other.size)
      }
    }
    
    fun main() {
      val cage = Cage5(mutableListOf(Eagle(), Sparrow()))
      cage.printAfterSorting()
    }

    두 리스트에 겹치는 원소가 하나라도 있는지 확인하는 함수

    fun <T> List<T>.hasIntersection(other: List<T>): Boolean {		//제네릭을 활용하면 모든 확장자에 사용 가능하다
      return (this.toSet() intersect other.toSet()).isNotEmpty()
    }

    타입 소거와 Star Projection

    코틀린은 언어 초기부터 제네릭이 고려되었기 때문에 raw type 객체를 만들 수 없다.

    //Java
    List list = new ArrayList<>();		//raw Type 인 List 로 가능
    
    //Kotlin
    var list:List = listOf(1,2,3) 		//raw Type 인 List 로 불가능

    타입 소거를 확인할 수 있는 대표적인 코드

    fun checkStringList(data: Any) {
      if (data is List<String>) { // 런타임 때는 String 정보가 사라지기에 List<String> 인지 알 수 없다.
      }
    }

     

    star projection

    fun checkStringList(data: Any) {
      if (data is List<*>) {
        // 여전히 리스트가 맞는지는 확인 가능
        // 그러나 리스트 내부의 타입은 확인 불가
        val element: Any? = data[0]	//그 데이터가 어떤 타입인지 모르기 때문에 가장 최상위 타입인 Any? 로 가져올 수 있다.
      }
    }
    
    
    fun checkMutableList(data: Any) {
      if (data is MutableList<*>) {
        data.add(3)		//ERROR : 타입 안전성을 헤치게 되어 add() 메소드는 사용할 수 없다.
      }
    }

     

    reified 와 inline

    //우리가 원하는 형태
    fun <T> List<*>.hasAnyInstanceOf(): Boolean {
      return this.any { it -> it is T }
    }
    
    //각 타입별로 만드는 방법
    fun List<*>.hasAnyInstanceOfString(): Boolean {
      return this.any { it is String }
    }
    
    fun List<*>.hasAnyInstanceOfInt(): Boolean {
      return this.any { it is Int }
    }
    
    //T의 정보를 가져오고 싶을 때
    inline fun <reified T> List<*>.hasAnyInstanceOf(): Boolean {	//inline 함수를 복사하게되고 reified 를 붙인다.
      return this.any { it is T }
    }

     

    타입 파라미터 섀도잉

    타입 파라미터 섀도잉(type parameter shadowing)은 동일한 이름의 타입 파라미터가 중첩된 스코프에서 사용될 때, 바깥 스코프의 타입 파라미터가 안쪽 스코프의 타입 파라미터에 의해 가려지는 현상을 의미

    • 타입 파라미터 섀도잉은 동일한 이름의 타입 파라미터가 중첩된 스코프에서 사용될 때 발생
    • 예제에서는 클래스 Container와 메서드 transform에서 동일한 이름의 타입 파라미터 T를 사용하여 섀도잉이 발생
    • 섀도잉 방지를 위해 각 스코프에서 고유한 이름의 타입 파라미터를 사용하는 것이 좋음
    class CageShadow<T : Animal> {
      fun <T : Animal> addAnimal(animal:T){
        //여기서 사용하는 <T> 는 전역변수의 T 를 가리게 된다(덮어씌어짐)
      }
    }
    fun main() {
      val cage = CageShadow<GoldFish>()
      cage.addAnimal(GoldFish("금붕어"))
      cage.addAnimal(Carp("잉어"))      //GoldFish 로 선언했는데 Carp 도 사용이 가능하다.
    }

    제네릭 클래스의 상속

    CageV1 클래스를 상속해 CageV2 를 만들어보자.

    open class CageV1<T : Animal> {
      fun addAnimal(animal: T){
    
      }
    }
    
    //방법 1
    class CageV2<T : Animal> : CageV1<T>()    //같은 제약 조건을 가져야만 한다. 타입 파라미터를 한번 받아서 올려준다.
    
    //방법 2
    class GoldFishCageV2 : CageV1<GoldFish>(){  //애초에 타입 파라미터를 명시적으로 정해놓는다.
      override fun addAnimal(animal: GoldFish){		//같은 GoldFish 를 자동적으로 적용
        super.addAnimal(animal)
      }
    }

    제네릭과 Type Alias

    typealias PersonDtoStore = Map<PersonDtoKey, MutableList<PersonDto>>	//PersonDtoStore 이름의 Type 을 생성
    
    fun handleCacheStore(store: PersonDtoStore){	//위에서 생성한 PersonDtoStore 를 적용
      
    }

    댓글

Designed by Tistory.