코드를 작성하다보면 다양한 타입에 동일한 로직을 적용하기 위해 코드의 재사용을 과도하게 하려는 경우가 있다. 예를들어, 파라미터를 전부 Any 타입으로 받는 등 타입의 안정성을 저하시킬 수 있다.
제네릭은 이러한 이슈에 적절한 균형을 맞춰준다.
즉, 제네릭을 사용하면 다양한 타입에서 사용 가능한 코드를 작성할 수 있다 !
또한, 코틀린 컴파일러는 제네릭 클래스 또는 함수가 의도하지 않은 타입에서 사용되는지를 검증할 수 있다.
자바에서의 제네릭은 기본적으로 타입 불변성을 강요했다.
정리하자면 제네릭 함수가 파라미터 타입 T를 받는다면, T의 부모 클래스나 자식 클래스를 사용하는 것이 불가능했다.
즉, 타입이 정확히 일치해야 했는데, 이러한 제약이 크게 걸릴 수록 안정성이 높아진다고 생각한다.
any로 공통모듈 사용하는 예시..
getCounselSpaceList(data: any): Promise<any[]> {
const param: object = {
mapCode: MAPPER.GET_COUNSEL_SPACE,
...data,
};
return new Promise((resolve, reject) => {
this._postApiService.select(param).then((res: any) => {
resolve(res);
}, reject);
});
}
open class Fruit // 부모 클래스
class Apple : Fruit() // 자식 클래스
class Banana : Fruit() // 자식 클래스
-- Apple과 Banana 클래스는 Fruit를 상속받는다.
fun receiveFruits(fruits: Array<Fruit> {
println("Number of fruits: ${fruits.size}")
}
제네릭 타입으로 Fruits 클래스를 지정한 배열(Array)을
파라미터(fruits)로 받는 receiveFruits() 함수를 생성하였다.
fun main() {
val fruits: Array<Apple> = arrayOf(Apple())
receiveFruits(fruits)
}
fun receiveFruits(fruits: Array<Fruit>) {
fruits[0] = Banana() // 문제가 될 수 있음!
}
타입 체크를 통해 fruits의 요소 타입이 Banana 일 경우에만 변경 가능하도록 구현을 할 수 있겠지만,
이러한 방식도 SOLID의 리스코프 치환 원칙에 위배된다.
fun receiveFruits(fruits: List<Fruit>) {
println("Number of fruits: ${fruits.size}")
}
fun main() {
val fruits: List<Apple> = listOf(Apple(), Apple())
receiveFruits(fruits) // Number of fruits: 2
}
출력 : Number of fruits:2
interface Cage<T> {
fun get(): T
}
open class Animal
open class Hamster(var name: String) : Animal()
class GoldenHamster(name: String) : Hamster(name)
fun tamingHamster(cage: Cage<out Hamster>) {
println("길들이기 : ${cage.get().name}")
}
fun main() {
val animal = object : Cage<Animal> {
override fun get(): Animal {
return Animal()
}
}
val hamster = object : Cage<Hamster> {
override fun get(): Hamster {
return Hamster("Hamster")
}
}
val goldenHamster = object : Cage<GoldenHamster> {
override fun get(): GoldenHamster {
return GoldenHamster("Leo")
}
}
tamingHamster(animal) // compile Error
tamingHamster(hamster)
tamingHamster(goldenHamster)
}
// tamingHamster 함수는 Hamster의 서브타입만을 받기때문에 animal 변수는 들어갈 수 없다.
interface Cage<T> {
fun get(): T
}
open class Animal
open class Hamster(var name: String) : Animal()
class GoldenHamster(name: String) : Hamster(name)
fun ancestorOfHamster(cage: Cage<in Hamster>) {
println("ancestor = ${cage.get()::javaClass.name}")
}
fun main() {
val animal = object : Cage<Animal> {
override fun get(): Animal {
return Animal()
}
}
val hamster = object : Cage<Hamster> {
override fun get(): Hamster {
return Hamster("Hamster")
}
}
val goldenHamster = object : Cage<GoldenHamster> {
override fun get(): GoldenHamster {
return GoldenHamster("Leo")
}
}
ancestorOfHamster(animal)
ancestorOfHamster(hamster)
ancestorOfHamster(goldenHamster) // compile Error
}
// ancestorOfHamster에서 햄스터의 조상을 찾는 함수를 구현하여 햄스터를 포함한 그 조상들만 허용하도록 제한하였다.
// 하위타입인 Cage<GoldenHamster>는 제한에 걸려있어 compile error가 나는것을 확인 해볼 수 있겠다.
interface Cage<T> {
fun get(): T
}
open class Animal
open class Hamster(var name: String) : Animal()
class GoldenHamster(name: String) : Hamster(name)
fun matingGoldenHamster(cage: Cage<GoldenHamster>) {
val hamster = GoldenHamster("stew")
println("교배 : ${hamster.name} & ${cage.get().name}")
}
fun main() {
val animal = object : Cage<Animal> {
override fun get(): Animal {
return Animal()
}
}
val hamster = object : Cage<Hamster> {
override fun get(): Hamster {
return Hamster("Hamster")
}
}
val goldenHamster = object : Cage<GoldenHamster> {
override fun get(): GoldenHamster {
return GoldenHamster("Leo")
}
}
matingGoldenHamster(animal) // compile Error
matingGoldenHamster(hamster) // compile Error
matingGoldenHamster(goldenHamster)
}
// 위 코드의 Cage<Animal>, Cage<Hamster>, Cage<GoldenHamster>는 서로 각각 연관이 없는 객체로서 무공변성의 적절한 예시이다.
가끔은 코틀린에게 타입 안정성을 희생하지 않고 약간의 제약을 풀어달라고 요청해야 할 때가 있을 것이다.
예를들어 코틀린 컴파일러가 공변성을 허용해서 제네릭 베이스 타입이 요구되는 곳에 제네릭 파생 타입이 허용되도록 하길 원한다는 것이다.
이럴 때는 타입 프로젝션(type projections)이 필요하다.
우선 예시를 통해 한번 더 정리해보자.
fun copyFromTo(from: Array<Fruit>, to: Array<Fruit>) {
for (i in from.indices) {
to[i] = from[i]
}
}
// copyFromTo() 함수는 from 배열의 객체를 순회하면서 to 배열로 값을 넣어주는 함수이다.
fun main() {
val fruitsBasket1 = Array<Fruit>(3) { _ -> Fruit() }
val fruitsBasket2 = Array<Fruit>(3) { _ -> Fruit() }
copyFromTo(fruitsBasket1, fruitsBasket2)
}
// 이 때 전달받은 두 배열의 크기가 동일하다고 가정하고 구동 시 별 문제 없이 작동한다.
// copyFromTo() 함수의 파라미터로 Fruit 타입의 배열로 정의했고, 그에 맞춰 넘겨줬기 때문이다.
fun copyFromTo(from: Array<Fruit>, to: Array<Fruit>) {
for (i in from.indices) {
to[i] = from[i]
}
}
// copyFromTo() 함수는 from 배열의 객체를 순회하면서 to 배열로 값을 넣어주는 함수이다.
fun main() {
val fruitsBasket1 = Array<Apple>(3) { _ -> Apple() }
val fruitsBasket2 = Array<Fruit>(3) { _ -> Fruit() }
copyFromTo(fruitsBasket1, fruitsBasket2) // type mismatch
}
// copyFromTo의 1번 파라미터로 Fruit 타입이 아닌 Apple 타입으로 fruitsBasket1을 넘겨줄 시 컴파일 에러가 발생한다.
fun copyFromTo(from: Array<out Fruit>, to: Array<Fruit>) {
for (i in from.indices) {
to[i] = from[i]
}
}
fun main() {
val fruitsBasket1 = Array<Apple>(3) { _ -> Apple() }
val fruitsBasket2 = Array<Fruit>(3) { _ -> Fruit() }
copyFromTo(fruitsBasket1, fruitsBasket2) // type missmatch
}
// 코틀린은 from 레퍼런스에 data 가 새로 들어가게 하는 메소드 호출이 없다는 사실을 확인하고 메소드 시그니처가 호출되는 것을 확인하여 이를 검증한다.
// 위 케이스에서 from 파라미터는 파라미터의 값을 읽기만 하기 때문에 Array<T>의 T에 Fruit 클래스나 Fruit 하위 클래스가 전달되더라도 아무런 위험이 없다.
// 이런 것을 타입이나 파생 타입에 접근하기 위한 파라미터 타입의 공변성이라고 한다.
// 하지만 이때 Array<<out Fruit>out Fruit> 으로 선언된 from 파라미터는 읽기만 가능할 뿐 내용을 변경하거나 추가하려 할 경우에는 컴파일 에러가 발생하게 된다.
예시
package chap04.section1
class Person<in T>(val age: Int)
fun main() {
//val anys: Person<Any> = Person<Int>(10)
val nothings: Person<Nothing> = Person<Int>(20) // 다운 캐스팅
}
공변성
open class Animal(val size: Int){
fun feed() = println("Feeding...")
}
class Cat(val jump: Int): Animal(50)
class Spider(val poison: Boolean): Animal(1)
class Box<in T>
fun main() {
val c1 = Cat(10)
val s1 = Spider(true)
var a1: Animal = c1
a1 = s1
println("s1 ${a1.size} ${a1.poison}")
val b1: Box<Cat> = Box<Animal>() // 다운 캐스팅
//val b2: Box<Animal> = Box<Cat>() // 업 캐스팅
val b3 = Box<Spider>()
}
반공변성
open class Animal(val size: Int){
fun feed() = println("Feeding...")
}
class Cat(val jump: Int): Animal(50)
class Spider(val poison: Boolean): Animal(1)
class Box<out T: Animal>
fun main() {
val c1 = Cat(10)
val s1 = Spider(true)
var a1: Animal = c1
a1 = s1
println("s1 ${a1.size} ${a1.poison}")
//val b1: Box<Cat> = Box<Animal>() // 다운 캐스팅
val b2: Box<Animal> = Box<Cat>() // 업 캐스팅
val b3 = Box<Spider>()
//val b4: Box<Number> = Box<Int>() // 업 캐스팅
}
무변성
open class Animal(val size: Int){
fun feed() = println("Feeding...")
}
class Cat(val jump: Int): Animal(50)
class Spider(val poison: Boolean): Animal(1)
class Box<out T>
fun main() {
val c1 = Cat(10)
val s1 = Spider(true)
var a1: Animal = c1
a1 = s1
println("s1 ${a1.size} ${a1.poison}")
//val b1: Box<Cat> = Box<Animal>()
val b2: Box<Animal> = Box<Cat>() // 업 캐스팅
val b3 = Box<Spider>()
}
-- 쉽게 정리하자면
읽기 전용은 안에 들어있는 값을 빼서 읽어야 하니까 out, 쓰기 전용은 새로운 값을 집어 넣어야 하니까 in 으로 외우자