-
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 를 적용 }
'kotlin' 카테고리의 다른 글
kotlin) 어노테이션과 리플렉션 (2) 2024.07.24 kotlin) 복잡한 함수형 크로그래밍 (0) 2024.07.20 kotlin) Kotlin 의 지연과 위임 (0) 2024.07.18 kotlin) 추가적으로 알아두어야 할 코틀린 특성 (0) 2024.07.13 kotlin) 코틀린에서의 FP (0) 2024.07.09