자바를 공부하고 적용하면서 추상화를 하기 위해 인터페이스를 적극 활용했었고, 강타입 언어였기에 특히 타입을 잘 활용했어야 했다.
그 과정에서 제네릭을 다시 한번 공부하게 되었고, 공변, 반공변 성 등의 특징을 제네릭이 어떻게 풀어냈는지 공부했다.
extend, super 등의 한정자를 사용하여 타입을 적절히 제어했었던 기억이 있다.
코틀린도 마찬가지로 제네릭의 한정자를 제공한다.
자바의 결함을 개선하면서 말이지.
Out 한정자
- 파라미터를 공변으로 만든다.
class University<out T>
open class Student
class FreshMan : Student()
fun main(args: Array<String>) {
val b: University<Student> = University<FreshMan>() // OK
val a: University<FreshMan> = University<Student>() // compile error
Student의 서브 타입이 FreshMan 이므로 최초 제네릭을 지정한 타입의 서브 타입만이 허용된다.
In 한정자
- 파라미터를 반공변으로 만든다.
class University<in T>
open class Student
class FreshMan : Student()
fun main(args: Array<String>) {
val b: University<Student> = University<FreshMan>() // compile error
val a: University<FreshMan> = University<Student>() // OK
최초 지정한 제네릭 타입의 슈퍼 타입 만이 허용된다.
Variance 한정자의 이점
자바 배열의 문제점
자바의 배열은 공변이다.
그렇기에 큰 문제가 발생한다.
가령,
Integer[] numbers = {1,2,3,4};
Object[] objects = numbers;
objects[4] = 'c'; // Runtime Exception 발생
이를 해결하기 위해 코틀린은 Array를 불공변으로 만들었다. Array<Int>, Array<String> 처럼~
out 한정자의 쓰임새
가령 이러한 클래스 구성이 있다고 생각하자.
open class Person
class Child: Person()
class Adult: Person()
class Box<out T> {
private var value: T? = null
fun set(value: T) {
this.value = value
}
fun get(): T = value ?: throw RuntimeException()
}
val adultBox = Box<Adult>()
val personBox: Box<Person> = adultBox
personBox.set(Child()) // ???? 난 Adult 만 넣고 싶은데,
val childBox = Box<Child>()
val box: Box<Any> = childBox
box.set("something else.") // 아니야 Child 전용 인데, 뜬금 없는 값이 들어가는 중.
box.set(23)
out 한정자는 이렇게 사용하는 것이 아니다.
들어가는 위치가 아니라 return 하는 위치에 적용하는 것이다.
이를테면,
Response 가 있다.
sealed class Response<out R, out E>
class Failure<out E>(val error: E): Response<Nothing, E>()
class Success<out R>(val value: R): Response<R, Nothing>()
두개의 제네릭이 out 한정자를 사용하였고, 실패와 성공 클래스에서 return 타입을 안정적으로 뱉을 수 있고, 오류 타입과 잠재적인 값을 지정하지 않을 수 있다.
in 한정자의 쓰임새
open class Car
interface Boat
class Amphibious: Car(), Boat
fun getAmphibious(): Amphibious = Amphibious()
val car: Car = getAmphibious()
val boat: Boat = getAmphibious()
in 한정자에 맞는 동작이 아닌 경우이다.
문제는 없지만.
in 한정자를 씌운 반공변성 타입의 파라미터를 public out 한정자 위치에 사용하면 안된다.
올바른 쓰임새로,
public interface Continuation<in T> {
private val context: CoroutineContext
private fun resumeWith(result: Result<T>)
}
이런 식으로 타입 파라미터 자리에 반공변인 in 한정자를 사용하자.
Variance 한정자의 위치
- 선언 부분
- 클래스, 인터페이스 선언에 한정자가 적용 되어 내부에 모든 영향을 준다.
- 가장 일반적인 경우
- 활용 부분
- 특정 변수에만 한정자가 적용된다.
- 특별한 변수에 적용하고 싶을 경우
정리
- 타입 파라미터는 기본적으로 불공변
- out 한정자
- 공변하게 만든다.
- return 되는 타입에 이용
- in 한정자
- 반공변하게 만든다.
- 허용만 되는 타입에 이용
'📚 Kotlin' 카테고리의 다른 글
[Effective Kotlin] 아이템 36. 상속보다는 컴포지션을 사용하라 (0) | 2023.01.30 |
---|---|
[Effective Kotlin] 아이템 32. 생성자 대신 팩토리 함수를 사용하라 (2) | 2023.01.29 |
[Effective kotlin] 아이템 27. 변화로부터 코드를 보호하려면 추상화를 사용하라 (0) | 2023.01.29 |
[Effective Kotlin] 아이템 23. 타입 파라미터의 섀도잉을 피하라 (0) | 2023.01.25 |
[감상문] Kotlin In Action을 읽고 (0) | 2023.01.17 |