[코틀린 인 액션] CH9 제네릭스

0

코틀린 인 액션

목록 보기
11/13
post-thumbnail

[코틀린 인 액션] CH9 제네릭스

이 포스팅은 <Kotlin in Action>, 드미트리 제메로프 & 스베트라나 이사코바, 에이콘출판사(2017)을 읽고 개인 학습용으로 정리한 글입니다.

9.1 제네릭 타입 파라미터

  • 제네릭스를 사용하면 타입 파라미터를 받는 타입 정의 가능

  • 제네릭 타입의 인스턴스를 만들려면 타입 파라미터를 구체적인 타입 인자로 치환해야

  • 제네릭 타입의 타입 인자 컴파일러 추론 가능
    -> 타입 인자 추론 근거 없는 경우(빈 List) 직접 타입 인자 명시해야

  • 리스트를 만들 때
    -> 변수 타입 지정 가능
    -> 변수를 만드는 함수의 타입 지정 가능

val list1 : MutableList<String> = mutableListOf()
val list2 = mutableListOf<String>()

9.1.1 제네릭 함수와 프로퍼티

fun <T> List<T>.slice(indices: IntRange):List<T>
  • 제네릭 함수를 정의할 때와 마찬가지로 제네릭 확장 프로퍼티 선언 가능
//리스트의 마지막 원소 바로 앞에 있는 원소 반환
val <T> List<T>.penultimate: T
	get() = this[size-2]
  • 일반 프로퍼티는 타입 파라미터를 가질 수 X

9.1.2 제네릭 클래스 선언

  • 제네릭 클래스를 확장하는 클래스를 정의하려면 기반 타입의 제네릭 파라미터에 대해 타입 인자 지정해야
    -> 구체적인 타입 넘길 수 O
    -> 하위 클래스도 제네릭 클래스라면, 타입 파라미터로 받은 타입 넘길 수 O

9.1.3 타입 파라미터 제약

  • 타입 파라미터 제약(type parameter constraint)

  • 어떤 타입을 제네릭 타입의 파라미터에 대한 상한(upper bound)로 지정
    -> 타입 인자는 반드시 그 상한 타입이거나 그 상한 타입의 하위 타입

fun <T:Number> oneHalf(value:T):Double{
	return value.toDouble() / 2.0
}
  • 타입 파라미터에 여러 제약 가하기
fun <T> ensureTrailingPeriod(seq:T) where T : CharSequence, T:Appendable{
    if(!seq.endsWith('.')){
        seq.append('.'
    }
}
  • 널 가능성을 제외한 아무런 제약도 필요 없는 경우
    -> 상한으로 Any 사용 <T:Any>
    -> T타입이 널이 될 수 없는 타입만 되도록 보장

9.2 실행 시 제네릭스의 동작: 소거된 타입 파라미터와 실체화된 타입 파라미터

9.2.1 실행 시점의 제네릭: 타입 검사와 캐스트

  • 코틀린 제네릭 타입의 타입 인자 정보는 실행 시점에 지워짐 -> 타입 소거
val list1: List<String> = listOf("a", "b", "c")
val list2: List<Int> = listOf(1, 2, 3)
  • 컴파일러는 두 리스트를 서로 다른 타입으로 인식

  • 실행 시점에서는 두 리스트 서로 다른 타입으로 인식 X
    -> 오직 List 객체로만 봄, List 객체가 어떤 타입의 원소 저장하는지 알 수 X

  • 실행 시점에서 is로 타입 인자로 지정한 타입 검사 -> 컴파일 오류

  • 실행 시점에서 as/as?로 타입 캐스팅 -> 성공하지만 경고(unchecked cast)

  • 인자를 알 수 없는 제네릭 타입 표현 -> 스타 프로젝션 사용

//컴파일 오류
if(value is List<String>){...}

//스타 프로젝션 사용
if(value is List<*>){...}

9.2.2 실체화한 타입 파라미터를 사용한 함수 선언

  • 인라인 함수의 파라미터는 실체화됨
    -> 함수 inline으로 바꾸고 파라미터 reified로 지정
    -> 실행 시간에 인라인 함수의 타입 인자 알 수 O

  • 컴파일러 인라인 함수의 본문을 함수가 호출되는 모든 지점에 삽입할 때
    -> 실체화한 인자 타입을 이용해 타입 인자로 쓰인 구체적인 클래스를 참조하는 바이트 코드 생성하여 삽입
    -> 실행 시점에 벌어지는 타입 소거 영향 X

  • 실체화된 파라미터를 사용하는 인라인 함수
    -> 자신에게 전달되는 모든 람다와 함께 인라이닝 됨

9.2.3 실체화한 타입 파라미터로 클래스 참조 대신

⚡9.2.4 실체화한 타입 파라미터 제약

  • 실체화한 파라미터 사용 가능한 경우
    • 타입 검사와 캐스팅(is, !is, as, as?)
    • 코틀린 리플렉션 API
    • 코틀린 타입에 대응하는 java.lang.Class 얻기(::class.java)
    • 다른 함수를 호출할 때 인자로 사용
  • 다음과 같은 일은 할 수 없다
    • 타입 파라미터의 인스턴스 생성하기
    • 타입 파라미터 클래스의 동반 객체 메서드 호출하기
    • 실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
    • 클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정하기

9.3 변성: 제네릭과 하위 타입

9.3.1 변성이 있는 이유: 인자를 함수에 넘기기

9.3.2 클래스 타입, 하위 타입

  • 제네릭 클래스가 아닌 클래스: 클래스 이름 바로 타입으로 사용 가능
    -> 널이 될 수 있는 타입, 널이 될 수 없는 타입 구성 가능

  • 제네릭 클래스: 제네릭 타입의 타입 파라미터를 구체적인 타입 인자로 바꿔줘야
    -> ex. List<Int>,List<String?>, ...

  • 어떤 타입 A의 값이 필요한 모든 장소에 어떤 타입 B의 값을 넣어도 아무런 문제가 없을 때
    -> 타입 B는 타입 A의 하위 타입

  • 간단한 경우 하위 타입은 하위 클래스와 근본적으로 같다
    -> ex. Int는 Number의 하위 클래스 -> Int는 Number의 하위 타입

  • 널이 될 수 없는 타입, 널이 될 수 있는 타입
    -> 두 타입의 클래스 같음
    -> 널이 될 수 없는 타입은 널이 될 수 있는 타입의 하위 타입

  • 제네릭 타입을 인스턴스화할 때 타입 인자로 서로 다른 타입이 들어갔을 때 인스턴스 타입 사이의 하위 타입 관계 성립하지 않을 때
    -> 무공변
    -> T 아무 위치(in/out)에서나 사용 가능

9.3.3 공변성: 하위 타입 관계를 유지

  • A가 B의 하위 타입일 때 Producer<A>가 Producer<B>의 하위 타입일 때
    -> 공변적

  • 제네릭 클래스가 타입 파라미터에 대해 공변적임을 표시하려면
    -> 타입 파라미터 이름 앞에 out

  • 클래스 멤버를 선언할 때 타입 파라미터를 사용할 수 있는 지점: in, out
    -> in 위치: 함수 파라미터 타입
    -> out 위치: 함수 반환 타입

  • 클래스 타입 파라미터 앞에 out 키워드를 붙이면
    -> 클래스 안에서 T를 사용하는 메서드가 out 위치에서만 T 사용 허용

  • ⭐out 키워드:

    • 공변성: 하위 타입 관계 유지
    • 사용 제한: T를 out 위치에서만 사용 가능

  • 타입 파라미터를 함수 파라미터 타입/함수 반환 타입으로만 사용 가능한 건 X
    -> 타입 파라미터를 다른 타입의 타입 인자로 사용 가능

  • MutableList<T>를 타입 T에 대해 공변적인 클래스로 선언할 수 X
    -> T를 인자로 받아서 그 타입의 값을 반환하는 메서드 존재
    -> T in, out 위치 동시에 쓰임

  • 타입 파라미터가 out이라 해도 생성자 파라미터 선언에 사용 가능

  • val/var 키워드를 생성자 파라미터에 적는다면 게터/세터를 정의하는 것과 같음
    -> 읽기 전용 프로퍼티: 게터(out 위치) 정의됨
    -> 변경 가능 프로퍼티: 게터(out 위치), 세터(in 위치) 정의됨 -> out으로 표시 불가능

  • 이런 in/out 위치 규칙은 외부에서 볼 수 있는 클래스(public, protected, internal) API에만 적용
    -> private 메서드의 파라미터는 in도 아니고 out도 아니다

9.3.4 반공변성: 뒤집힌 하위 타입 관계

  • 타입 B가 타입 A의 하위 타입인 경우 Consumer<A>가 Consumer<B>의 하위 타입일 때
    -> 반공변성
    -> 하위 타입 관계 뒤집힘

  • ex. 어떤 타입의 객체를 Comparator로 비교해야할 때
    -> 그 타입이나 그 타입의 조상을 비교할 수 있는 Comparator 사용 가능
    -> Comparator<Any>가 Comparator<String>의 하위 타입

  • 제네릭 클래스가 타입 파라미터에 대해 반공변적임을 표시하려면
    -> 타입 파라미터 이름 앞에 in

  • ⭐in 키워드:

    • 반공변성: 하위 타입 관계 반전
    • 사용 제한: T를 in 위치에서만 사용 가능

interface Function1<in P, out R>{
    operator fun invoke(p:P):R
}
  • 코틀린 표기에서 (P)->R은 Function1<P, R>을 알아보기 쉽게 표현한 것
    -> P(함수 파라미터 타입): 오직 in 위치에서만 사용 가능, 반공변젹
    -> R(함수 반환 타입): 오직 out 위치에서만 사용 가능, 공변적

9.3.5 사용 지점 변성: 타입이 언급되는 지점에서 변성 지정

  • 타입 파라미터가 있는 타입을 사용할 때마다 해당 타입 파라미터를 하위 타입이나 상위 타입 중 어떤 타입으로 대치할 수 있는지 명시
    -> 사용 지점 변성(use-site variance)
    -> 함수 정의 시 파라미터에 변성 변경자 추가

  • 이때 타입 프로젝션이 일어난다

9.3.6 스타 프로젝션: 타입 인자 대신 * 사용

  • 제네릭 타입 인자 정보 없음을 표현하기 위해 사용

  • 타입 파라미터를 시그니처에 전혀 언급하지 않거나 데이터를 읽기는 하지만 그 타입에 관심이 없는 경우에도 사용

  • MutableList<*>는 MutableList<Any?>와 같지 않다
    -> MutableList<Any?>: 모든 타입의 원소를 담을 수 있음
    -> MutableList<*>: 어떤 정해진 구체적인 타입의 원소만 담을 수 있음, 그 원소의 타입을 정확히 모름

  • 컴파일러는 MutableList<*>를 MutableList<Any?>의 아웃 프로젝션 타입으로 인식
    -> 어떤 리스트의 원소 타입을 모르면 그 리스트에 원소를 넣을 수 X
    -> 어떤 리스트의 원소 타입을 모르더라도 그 리스트에서 Any?타입의 원소 꺼내올 수 O

📌참고자료

  • A type projection is a type that has been limited in certain ways in order to gain variance characteristics.
  • When you want subtyping to apply to your generic types, Kotlin gives you two options:
    • Declaration-site variance - you can specify the variance within the definition of the class or interface.
    • Use-site variance - you can specify the variance at the places in your code where you’re using an instance of a generic.
  • Type projection is the result of use-site variance.
  • When you apply use-site variance to an object, that object’s type is projected to a more limited view of the type.
profile
Be able to be vulnerable, in search of truth

0개의 댓글