일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- rxjava disposable
- 2022 플러터 설치
- ANR이란
- android ar 개발
- rxjava hot observable
- 스택 큐 차이
- 2022 플러터 안드로이드 스튜디오
- Rxjava Observable
- 안드로이드 유닛 테스트 예시
- 안드로이드 라이선스
- 안드로이드 레트로핏 사용법
- 플러터 설치 2022
- rxjava cold observable
- 객체
- 안드로이드 레트로핏 crud
- 클래스
- 안드로이드 os 구조
- jvm이란
- ar vr 차이
- 큐 자바 코드
- 스택 자바 코드
- 안드로이드 유닛 테스트
- 멤버변수
- 서비스 쓰레드 차이
- 서비스 vs 쓰레드
- 자바 다형성
- android retrofit login
- jvm 작동 원리
- 안드로이드 유닛테스트란
- 안드로이드 라이선스 종류
- Today
- Total
나만을 위한 블로그
[이펙티브 코틀린] 아이템 24. 제네릭 타입과 variance 한정자를 활용하라 본문
아래 제네릭 클래스가 있다고 가정한다.
class Cup<T>
위 코드에서 타입 파라미터 T는 variance 한정자(out 또는 in)가 없기 때문에 기본적으로 invariant(불공변성)이다. invariant라는 건 제네릭 타입으로 만들어지는 타입들이 서로 관련성이 없다는 의미다. 예를 들어 Cup<Int>, Cup<Number>, Cup<Any>, Cup<Nothing>은 어떤 관련성도 갖지 않는다.
class Cup<T>
fun main() {
val anys: Cup<Any> = Cup<Int> // 오류. Type mismatch
val nothings: Cup<Nothing> = Cup<Int>() // 오류. Type mismatch
}
만약 어떤 관련성을 원한다면 out 또는 in이란 variance 한정자를 붙인다. out은 타입 파라미터를 covariant(공변성)로 만든다. 이는 A가 B의 서브타입일 때 Cup<A>가 Cup<B>의 서브타입이란 의미다.
class Cup<out T>
open class Dog
class Puppy: Dog()
fun main() {
val b: Cup<Dog> = Cup<Puppy>() // 오류 없음
val a: Cup<Puppy> = Cup<Dog>() // 오류. Type mismatch
val anys: Cup<Any> = Cup<Int>()
val nothings: Cup<Nothing> = Cup<Int>() // 오류. Type mismatch
}
in 한정자는 타입 파라미터를 contravariant(반변성)으로 만든다. A가 B의 서브타입일 때 Cup<A>가 Cup<B>의 슈퍼타입이라는 걸 의미한다.
class Cup<in T>
open class Dog
class Puppy: Dog()
fun main() {
val b: Cup<Dog> = Cup<Puppy>() // 오류. Type mismatch
val a: Cup<Puppy> = Cup<Dog>() // 오류 없음
val anys: Cup<Any> = Cup<Int>() // 오류. Type mismatch
val nothings: Cup<Nothing> = Cup<Int>() // 오류 없음
}
함수 타입
함수 타입은 파라미터 유형과 리턴 타입에 따라 서로 어떤 관계를 갖는다. 예를 들어 Int를 받고 Any를 리턴하는 함수를 파라미터로 받는 함수를 생각해 보라.
fun printProcessedNumber(transition: (Int) -> Any) {
print(transition(42))
}
(Int) -> Any 타입의 함수는 (Int) -> Number, (Int) -> Any, (Number) -> Number, (Number) -> Int 등으로도 작동한다.
val intToDouble: (Int) -> Number = { it.toDouble() }
val numberAsText: (Number) -> Any = { it.toShort() }
val identity: (Number) -> Number = { it.toInt() }
val numberHash: (Any) -> Number = { it.hashCode() }
fun main() {
printProcessedNumber(intToDouble)
printProcessedNumber(numberAsText)
printProcessedNumber(identity)
printProcessedNumber(numberHash)
}
타이핑 시스템 계층에서 파라미터 타입이 더 높은 타입으로 이동하고 리턴 타입은 계층 구조의 더 낮은 타입으로 이동한다.
코틀린 함수 타입의 모든 파라미터 타입은 contravariant다. 또한 모든 리턴 타입은 covariant다. 함수 타입을 쓸 때는 자동으로 variance 한정자가 사용된다.
variance 한정자의 안정성
자바의 배열은 covariant다. 많은 출처에 따르면 이는 배열을 기반으로 제네릭 연산자는 정렬 함수를 만들기 위함이라고 말한다. 그런데 자바의 배열이 covariant란 속성을 갖기 때문에 문제가 발생한다.
아래 코드는 컴파일 중에 아무 문제도 없지만 런타임 에러가 발생한다.
public class JavaTest {
public static void main(String[] args) {
Integer[] numbers = {1, 4, 2, 1};
Object[] objects = numbers;
objects[2] = "B"; // 오류 : Storing element of type 'java.lang.String' to array of 'java.lang.Integer' elements will produce 'ArrayStoreException'
}
}
numbers를 Object[]로 캐스팅해도 구조 내부에서 쓰이고 있는 실질적인 타입이 바뀌는 건 아니다. 여전히 Integer다. 따라서 이런 배열에 String 타입의 값을 할당하면 오류가 발생한다. 자바의 명백한 결함이다.
코틀린은 이런 결함을 해결하기 위해 Array(IntArray, CharArray 등)를 invariant로 만들었다. 따라서 Array<Int>를 Array<Any> 등으로 바꿀 수 있다.
파라미터 타입을 예측할 수 있다면 어떤 서브타입이라도 전달할 수 있다. 따라서 아규먼트 전달 시 암묵적으로 업캐스팅할 수 있다.
open class Dog
class Puppy: Dog()
class Hound: Dog()
fun takeDog(dog: Dog) {}
fun main() {
takeDog(Dog())
takeDog(Puppy())
takeDog(Hound())
}
이는 covariant하지 않다. covariant 타입 파라미터(out 한정자)가 in 한정자 위치에 있다면 covariant와 업캐스팅을 연결해서 내가 원하는 타입을 아무거나 전달할 수 있다.
즉 value가 구체적인 타입이라 안전하지 않으므로 value를 Dog 타입으로 지정할 경우 String 타입을 넣을 수 없다.
open class Dog
class Puppy: Dog()
class Hound: Dog()
class Box<out T> {
private var value: T? = null
// 코틀린에서 쓸 수 없는 코드
fun set(value: T) {
this.value = value
}
fun get(): T = value ?: error("Value not set")
}
fun main() {
val puppyBox = Box<Puppy>()
val dogBox: Box<Dog> = puppyBox
dogBox.set(Hound()) // Puppy를 위한 공간
val dogHouse = Box<Dog>()
val box: Box<Any> = dogHouse
box.set("Some string") // Dog를 위한 공간
box.set(42) // Dog를 위한 공간
}
이런 상황은 안전하지 않다. 캐스팅 후에 실질적인 객체가 그대로 유지되고 타이핑 시스템에서만 다르게 처리되기 때문이다. Int를 설정하려고 하는데 해당 위치는 Dog만을 위한 자리다. 만약 이게 가능하다면 오류가 발생할 것이다. 그래서 코틀린은 public in 한정자 뒤에 covariant 타입 파라미터(out 한정자)가 오는 걸 금지해서 이런 상황을 막는다.
class Box<out T> {
var value: T? = null // 오류
fun set(value: T) { // 오류
this.value = value
}
fun get(): T = value ?: error("Value not set")
}
가시성을 private로 제한하면 오류가 발생하지 않는다. 객체 내부에선 업캐스트 객체에 covariant(out 한정자)를 쓸 수 없기 때문이다.
class Box<out T> {
private var value: T? = null // 오류
private fun set(value: T) { // 오류
this.value = value
}
fun get(): T = value ?: error("Value not set")
}
covariant(out 한정자)는 public out 한정자 위치에서도 안전하므로 따로 제한되지 않는다. 이런 안정성의 이유로 생성되거나 노출되는 타입에만 covariant(out 한정자)를 쓰는 것이다. 이런 프로퍼티는 일반적으로 producer 또는 immutable 데이터 홀더에 많이 쓰인다. 예시로 T는 covariant인 List<T>가 있다. 지금까지 설명한 이유로 함수의 파라미터가 List<Any?>로 예측된다면 별도 변환 없이 모든 종류를 파라미터로 전달할 수 있다. 다만 MutableList<T>에서 T는 in 한정자 위치에서 쓰이며 안전하지 않으므로 invariant다.
fun append(list: MutableList<Any>) {
list.add(42)
}
fun main() {
val strs = mutableListOf("A", "B", "C")
append(strs) // 코틀린에서 쓸 수 없는 코드
val str: String = strs[3]
print(str)
}
다른 예로 Response가 있다. Response를 쓰면 다양한 이득을 얻을 수 있다. variance 한정자 때문에 아래 내용은 모두 참이 된다.
- Response<T>라면 T의 모든 서브타입이 허용된다. Response<Any>가 허용된다면 Response<Int>, Response<String>이 허용된다
- Response<T1, T2>라면 T1, T2의 모든 서브타입이 허용된다
- Failure<T>라면 T의 모든 서브타입 Failure가 허용된다. Failure<Number>라면 Failure<Int>, Failure<Double>이 모두 허용된다
- Failure<Any>라면 Failure<Int>, Failure<String>이 모두 허용된다
아래 코드는 covariant와 Nothing 타입으로 인해 Failure는 오류 타입을 지정하지 않아도 되고 Success는 잠재적인 값을 지정하지 않아도 된다.
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>()
covariant와 public in 위치 같은 문제는 contravariant 타입 파라미터(in 한정자)와 public out 위치(함수 리턴 타입 또는 프로퍼티 타입)에서도 발생한다. out 위치는 암묵적인 업캐스팅을 허용한다.
open class Car
interface Boat
class Amphibious: Car(), Boat
fun getAmphibious(): Amphibious = Amphibious()
val car: Car = getAmphibious()
val boat: Boat = getAmphibious()
이는 contravariant(in 한정자)에 맞는 동작이 아니다. 아래 코드는 어떤 상자(Box 인스턴스)에 어떤 타입이 들어 있는지 확실하게 알 수 없다.
open class Car
interface Boat
class Amphibious: Car(), Boat
fun getAmphibious(): Amphibious = Amphibious()
val car: Car = getAmphibious()
val boat: Boat = getAmphibious()
class Box<in T> {
// 코틀린에서 쓸 수 없는 코드
val value: T
}
fun main() {
val garage: Box<Car> = Box(Car())
val amphibiousSpot: Box<Amphibious> = garage
val boat: Boat = garage.value // Car를 위한 공간
val noSpot: Box<Nothing> = Box<Car>(Car())
val boat: Nothing = noSpot.value
// 아무것도 만들 수 없음
}
이런 상황을 막기 위해 코틀린은 contravariant 타입 파라미터(in 한정자)를 public out 한정자 위치에 쓰는 걸 금지하고 있다.
class Box<in T> {
val value: T? = null // 오류
fun set(value: T) {
this.value = value
}
fun get(): T = value // 오류
?: error("Value not set")
}
이번에도 요소가 private면 문제가 없다.
class Box<in T> {
private var value: T? = null // 오류
fun set(value: T) {
this.value = value
}
private fun get(): T = value // 오류
?: error("Value not set")
}
이런 형태로 타입 파라미터에 contravariant(in 한정자)를 사용한다. 추가적으로 많이 쓰이는 예로는 kotlin.coroutines.Continuation이 있다.
variance 한정자의 위치
variance 한정자는 크게 두 위치에 쓸 수 있다.
- 선언 부분 : 일반적으로 이 위치에 사용함. 여기서 쓰면 클래스, 인터페이스 선언에 한정자가 적용되기 때문에 클래스, 인터페이스가 쓰이는 모든 곳에 영향을 준다
// 선언 쪽의 variance 한정자
class Box<out T>(val value: T)
val boxStr: Box<String> = Box("string")
val boxAny: Box<Any> = boxStr
- 클래스, 인터페이스를 활용하는 위치 : 이 위치에 variance 한정자를 쓰면 특정 변수에만 variance 한정자가 적용된다
class Box<T>(val value: T)
val boxStr: Box<String> = Box("string")
// 사용하는 쪽의 variance 한정자
val boxAny: Box<out Any> = boxStr
모든 인스턴스에 variance 한정자를 적용하면 안 되고 특정 인스턴스에만 적용해야 할 때 이런 코드를 사용한다.
예를 들어 MutableList는 in 한정자를 포함하면 요소를 리턴할 수 없기 때문에 in 한정자를 붙이지 않는다. 하지만 단일 파라미터 타입에 in 한정자를 붙여서 contravariant를 갖게 하는 건 가능하다. 이렇게 하면 여러 타입을 받아들이게 할 수 있다.
interface Dog
interface Cutie
data class Puppy(val name: String): Dog, Cutie
data class Hound(val name: String): Dog
data class Cat(val name: String): Cutie
fun fillWithPuppies(list: MutableList<in Puppy>) {
list.add(Puppy("Jim"))
list.add(Puppy("Beam"))
}
fun main() {
val dogs = mutableListOf<Dog>(Hound("Pluto"))
fillWithPuppies(dogs)
println(dogs)
val animals = mutableListOf<Cutie>(Cat("Felix"))
fillWithPuppies(animals)
println(animals)
}
참고로 variance 한정자를 쓰면 위치가 제한될 수 있다. MutableList<out T>가 있다면 get으로 요소를 추출했을 때 T 타입이 나올 것이다. 하지만 set은 Nothing 타입의 아규먼트가 전달될 거라 예상되므로 사용할 수 없다. 이는 모든 타입의 서브타입을 가진 리스트 (Nothing 리스트)가 존재할 가능성이 있기 때문이다. MutableList<in T>를 사용할 경우 get, set을 모두 사용할 수 있다. 하지만 get을 사용할 경우 전달되는 자료형은 Any?가 된다. 모든 타입의 슈퍼타입을 가진 리스트(Any 리스트)가 존재할 가능성이 있기 때문이다.
'책 > Effective Kotlin' 카테고리의 다른 글
[이펙티브 코틀린] 아이템 26. 함수 내부의 추상화 레벨을 통일하라 (0) | 2023.01.09 |
---|---|
[이펙티브 코틀린] 아이템 25. 공통 모듈을 추출해서 여러 플랫폼에서 재사용하라 (0) | 2022.11.09 |
[이펙티브 코틀린] 아이템 23. 타입 파라미터의 섀도잉을 피하라 (0) | 2022.09.13 |
[이펙티브 코틀린] 아이템 22. 일반적인 알고리즘 구현 시 제네릭을 써라 (0) | 2022.09.12 |
[이펙티브 코틀린] 아이템 21. 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라 (0) | 2022.09.11 |