반응형
Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- 안드로이드스튜디오
- viewmodel
- Compose
- 안드로이드
- 스레드
- mockito
- 커스텀상태
- theming
- 병렬프로그래밍
- 병럴프로그래밍
- 코틀린
- 알게되는
- Gradle
- Kotlin
- 책
- android
- 안드로이드강좌
- 디자인패턴
- 코루틴
- Rxjava
- g 단위테스트
- 회고
- 글또
- 테스트
- 알고리즘
- k8s
- ReactiveProgramming
- 자바
- Coroutine
- kotlin강좌
Archives
- Today
- Total
선생님, 개발을 잘하고 싶어요.
Kotlin in Action - 2부 9장 - 제네릭스 본문
다루는 거
- 코틀린 제네릭과 자바 제네릭의 유사한 부분
- 런타임 파라미터 타입 소거 - type erasure
- 실체화된 타입 파라미터 - reified type
- 타입 파라미터의 상위하위 관계에 따른 두 제네릭 타입의 상위하위 관계, 변성 -
- variance: 변성 → 타입 인자의 상하 관계에 따라 제네릭 타입의 상하 관계가 어떻게 처리되는가? 에 대한 개념
- invariant: 무변성 → 타입 인자의 상하 관계가 제네릭 타입의 상하 관계와 관련 없음
- covariant: 공변성 → 타입 인자의 상하 관계가 제네릭 타입의 상하 관계와 같음
- contravariance: 반공변성 → 타입 인자의 상하 관계와 제네릭 타입의 상하 관계가 역전됨
- 선언 지점 변성 - declaration-site variance
- 사용 지점 변성 - use-site variance
- 스타 프로젝션 - 제네릭 타입 인자 타입 인자 정보 없고, 어떤 타입인지 중요하지 않을 때
제네릭 타입 파라미터
- 리스트를 다루는 함수를 작성할 때, 특정 타입을 저장하는 리스트뿐 아니라 모든 리스트를 다룰 수 있는 함수를 원할 수 있다. 이럴 때 사용하게된다.
- 실제로 이러한 제네릭 함수를 호출할 때 구체적 타입을 타입 인자로 넘겨야한다.
- 타입 파라미터를 수신 객체와 반환 타입도 사용한다.
- 메서드, 확장 함수, 최상위 함수에서 타입 파리미터 선언 가능
- 확장 프로퍼티도 제네릭 하게 만들 수 있다 반면 일반 프로퍼티는 불가능한데, 실제 인스턴스가 여러 타입의 값을 저장하는 게 이상하기 때문이다.
타입 파라미터 제약
- 클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능이다.
- 어떤 타입을 타입 파라미터에 대한 상한(upper bound)으로 지정하면, 그 제네릭 타입을 인스턴스화 할 때 사용하는 타입 인자는 반드시 그 상한 타입이거나 하위 타입이여야 한다.
fun <T : Number> List<T>.sum(): T // T가 타입 파라미터, Number가 상한
println(listOf(1,2,3).sum()) // Int는 Number의 하위 타입이므로 okay
println(listOf("kotlin", "java").sum()) // String은 Number의 하위 타입이 아니므로 컴파일 에러
- 정의한 함수 내부에서는 T를 상한 타입으로 취급하고 사용할 수 있다.
- 둘 이상의 제약을 달성하고 싶다면 where 키워드를 사용한다.
fun <T> ensureTrailingPeriod(seq: T)
where T : CharSequence, T : Appendable { ... {
- 타입 인자가 반드시 CharSequence, Appendable 인터페이스를 구현해야함을 의미한다.
- Any 타입을 상한으로 걸어서 타입 파라미터가 널이 될 수 없도록 지정할 수 있다. 혹은 nullable 하지 않은 SomeType 을 상한으로 걸어도 마찬가지 효과 달성 가능하다.
실행 시 제네릭스 동작: 소거된 타입 파라미터 & 실체화된 타입 파라미터
- JVM 제네릭스는 보통 타입 소거(type erasure)를 사용해 구현된다.
- 런타임에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않다는 뜻
타입 검사와 캐스트
- 제네릭 타입 인자 정보는 런타임에 지워진다. 즉, 제네릭 클래스 인스턴스가 그 인스턴스를 생성할 때 쓰인 타입 인자에 대한 정보를 유지하지 않는다는 뜻이다.
- List<Int>, List<String> 타입 모두 런타임에는 List 타입으로만 식별된다.
- 컴파일러는 두 리스트를 서로 다른 타입으로 인식
- 하지만 런타임은 그 둘을 완전히 같은 타입으로 인식
- 컴파일러가 타입 인자를 알고 올바른 타입의 값만 각 리스트에 넣도록 보장하기 때문에 개발할 때 상관이 없던 것임
- 그로 인해 런타임에 수행되는 is, as, as? 와 같은 타입 검사, 타입 캐스팅이 제네릭 타입 파라미터와 함께 쓰일 때 문제가 생긴다.
- is로 타입 체크는 컴파일러가 금지시킨다. (안전하지 못한 is 금지)
- as, as?는 다른 타입으로 캐스팅해도 여전히 캐스팅에 성공할 수 있다. (위험한 as 캐스팅 경고)
fun printSum(c: Collection<*>) { val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected") println(intList.sum()) } // Set은 List가 아니므로 IllegalArgumentException 발생 printSum(setOf(1,2,3)) // List<String>, List<Int> 모두 런타임에는 List 이므로 캐스팅 코드는 정상 동작 // intList.sum() 호출 시점에 ClassCastException 발생 printSum(listOf("a", "b", "c"))
실체화한 타입 파라미터를 사용한 함수 선언
- inline 함수 안에서는 타입 인자를 사용할 수 있다.
- 인라인 함수의 타입 파라미터를 실체화되므로 실행 시점에 인라인 함수의 타입 인자를 알 수 있다.
- 인라인 함수로 만들고 타입 파라미터에 reified를 지정하자.
- 자바 코드에서는 reified 타입 파라미터를 사용하는 inline 함수를 호출할 수 없다.
- 컴파일러가 실체화한 타입 인자를 사용해 인라인 함수 호출하는 각 부분의 정확한 타입 인자를 알고 바이트코드 생성 해서 삽입할 수 있다.
- inline 함수를 성능 향상 목적이 아니라, 실체화한 타입 파라미터를 사용하기 위해서도 쓴다.
변성: 제네릭과 하위 타입
클래스, 타입, 하위 타입
- 타입: 그 변수를 담을 수 있는 값의 집합
- String, String? 는 하나의 클래스(String)를 가지고 두 개의 타입을 표현한다.
- 제네릭 클래스에서 올바른 타입을 얻으려면 타입 파라미터를 구체적 타입 인자로 바꿔야 한다.
- 하나의 제네릭 클래스는 무수히 많은 타입을 만들어낼 수 있다.
- 하위 타입: A의 값이 필요한 모든 장소에 어떤 타입 B의 값을 넣어도 아무 문제가 없다면 타입 B는 타입 A의 하위 타입이다.
- 집합의 포함관계로 설명되는 관계다. B의 임의의 인스턴스는 A 집합에 포함된다는 의미다.
- 상위 타입은 하위 타입의 반대 관계다.
- 널이 될 수 없는 타입은 널이 될 수 있는 타입의 하위 타입이다.
- 널이 될 수 없는 타입에 null을 못 넣으니 이건 당연하다.
변성이 있는 이유: 인자를 함수에 넘기기
fun printContents(list: List<Any>) {
println(list.joinToString())
}
printContents(listOf("abc", "bac")) // 잘 동작한다.
fun addAnswer(list: MutableList<Any>) {
list.add(42)
}
val strings = mutableListOf("abc", "bac")
// 이 라인이 컴파일 되면, MutableList<String> 타입에 Int가 추가되는 이상한 일이 벌어진다.
// 고로 컴파일 되면 안된다.
addAnswer(strings)
- 리스트의 원소를 추가하거나 변경한다면 타입 불일치가 생길 수 있다. (List<Any> 대신 List<String>을 넘길 수 없음)
- 원소 추가나 변경이 없는 경우에는 List<String>을 List<Any> 대신 넘겨도 괜찮다.
- 이를 정리해 놓은 것이 앞으로 살필 코틀린의 변성이다.
공변성: 하위 타입 관계를 유지
- 공변성 클래스 Producer<T> 로 예시를 들자.
- A가 B의 하위 타입일 때, Producer<A>가 Producer<B>의 하위 타입이다.
interface Producer<out T> { // 클래스가, T에 대해 공변적이라고 선언한다.
fun produce(): T
}
- 타입 안전성을 보장하기 위해 공변적 파라미터는 항상 아웃 위치에만 있어야 한다.
- T 타입의 값을 생산할 수는 있지만, 소비할 수는 없다는 뜻이다.
- 아웃 위치에 있다? → T 값을 생산한다. (소비하지 못함)
- 인 위치에 있다? → T 값을 소비한다. (생산하지 못함)
- 타입 파라미터 T에 붙은 out 키워드는 다음 두 가지를 의미한다.
- 공변성: 하위 타입 관계가 유지된다.
- 사용 제한: T를 아웃 위치에서만 사용할 수 있다.
- variance 규칙은 클래스 외부의 사용자가 클래스를 잘못 사용하는 일을 막기 위한 것으로, 내부 구현에는 적용되지 않는다.
반공변성: 뒤집힌 하위 타입 관계
- 공변 클래스의 경우와 반대
- Comparator<String>를 구현하면, String의 하위 타입에 속하는 모든 값을 비교할 수 있다.
- Comparator<Any>가 있다면 이를 사용해 모든 타입의 값을 비교할 수 있다.
- Comparator<String>을 요구하는 함수에는 String보다 더 일반적인 타입을 비교할 수 있는 Comparator를 넘기는 것은 안전하다. (여전히 String 보다 구체 타입은 비교할 수 있을 것이므로)
- 타입 B가 타입 A의 하위 타입인 경우 Consumer<A>가 Consumer<B>의 하위 타입인 관계가 성립될 때, Consumer는 타입 인자 T에 대해 반공변이다. (contravariance)
interface Comparator<in T> {
fun compare(e1: T, e2: T): Int
}
- in 키워드가 붙은 타입이 클래스의 메서드 안으로 전달돼 메서드에 의해 소비된다는 뜻
- 클래스가 어떤 타입에 대해서는 공변적이면서 동시에 다른 타입에 대해서는 반공변적일 수도 있다.
interface Function1<in P, out R> {
operator fun invoke(p: P): R
}
fun something(f: (Cat) -> Number) { ... }
val somef: ((Animal) -> Int) = { ... }
// P는 반공변이므로, Cat의 상위 타입인 Animal을 지정해도 괜찮고
// R은 공변이므로, Number의 하위 타입인 Int를 지정해도 괜찮다.
something(somef) // 따라서 이 코드는 적법하다.
- 이런 식으로 클래스 정의에 변성을 직접 기술하면 그 클래스를 사용하는 모든 장소에 그 변성이 적용된다.
- 이를 선언 지점 변성 (declaration-site variance)라고 한다.
사용 지점 변성: 타입이 언급되는 지점에서 변성 지정
- 선언 지점 변성을 사용하면, 변성 변경자를 단 한 번만 표시하고 클래스를 쓰는 쪽에서는 변성에 대해 신경쓸 필요가 없으므로 코드가 더 간결해진다. 자바에는 그게 없으니, 와일드카드를 사용해 변성을 지정하는 코드가 반복된다.
- 코틀린도 여전히 특정 타입 파라미터가 나타나는 지점에서 변성을 정할 수 있다.
- 타입 파라미터를 사용하는 위치라면 어디에나 변성 변경자를 붙일 수 있다.
- 이렇게 in, out 변경자를 붙이면, 타입 프로젝션(type projection)이 일어난다.
- in 변경자를 사용한 해당 타입 파라미터를 in 위치에 사용하는 메서드만 호출 가능
- 이미 List<out T>인 타입에 대해 out 프로젝션 하는 것은 의미없다.
- 타입 파라미터가 쓰이는 위치에 in을 붙여서 그 위치에 있는 값이 소비자 역할을 한다고 표시할 수 있다.
- MutableList<out T>는 자바의 MutableList<? extends T>와 같다.
- MutableList<in T>는 자바의 MutableList<? super T>와 같다.
스타 프로젝션: 타입 인자 대신 * 사용
- List<*>
- 리스트지만 그 원소의 타입을 정확히 모른다는 사실을 표현
- 컴파일러는 MutableList<*>를 아웃 프로젝션 타입으로 인식한다.
- 원소 타입은 몰라도 리스트에서 안전하게 Any? 타입의 원소를 꺼내올 수 있기 때문
- MyType<*>는 자바의 MyType<?>에 대응한다.
- 같이 타입 인자 정보가 중요하지 않을 때도 스타 프로젝션 구문 사용 가능
'일상 > 책 리뷰' 카테고리의 다른 글
Kotlin in Action - 2부 11장 - DSL 만들기 (0) | 2022.06.07 |
---|---|
Kotlin in Action - 2부 10장 - 애노테이션과 리플렉션 (0) | 2022.06.06 |
Kotlin in Action - 2부 8장 - 고차 함수: 파라미터와 반환 값으로 람다 사용 (0) | 2022.05.28 |
Kotlin in Action - 2부 7장 - 연산자 오버로딩과 기타 관례 (convention) (0) | 2022.05.24 |
Kotlin in Action - 1부 6장 - 코틀린 타입 시스템 (0) | 2022.05.23 |
Comments