ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 9장 -제네릭스
    Kotlin/코틀린인액션요약 2022. 9. 20. 00:01
    728x90

    9장에서 다루는 내용

    제네릭 함수와 클래스 정의하는 방법

    타입 소거와 실체화한 타입 파라미터

    선언 지점과 사용 지점 변성

     

    코틀린에서 제네릭 클래스와 함수를 선언하고 사용하는 기본 개념은 자바와 비슷합니다.

    실제로 제네릭스가 쓰였던 일부 예제에서도 괴리감이 느껴지지 않았습니다.

     

    제네릭 타입 파라미터

    타입 파라미터를 받을 때 받는 타입을 정의할 수 있습니다.

    만약 문자열을 담는 리스트를 표현할 때 List <String>으로 표현합니다.

     

    코틀린 컴파일러는 타입 인자도 추론할 수 있습니다.

    val authors = listOf("Dmitry", "Svetlana")

    listOf에 전달된 두 값이 문자열이므로 컴파일러는 List<String>임을 추론합니다.

     

     

    제네릭 함수와 프로퍼티

    fun <T> List<T>.slice(indices: IntRange) : List<T>

    함수의 타입 파라미터 T가 수신 객체로 반환 타입에 쓰입니다.

    하지만 대체로 컴파일러가 타입 인자를 추론할 수 있습니다.

     

    타입 파라미터 제약

    클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능입니다.

    예를 들어 리스트에 속한 모든 원소의 합을 구하는 sum 함수를 생각해보겠습니다.

     

    List<Int> 나 List<Double>에는 sum 함수를 적용할 수 있지만 List<String>등에는 그 함수를 적용할 수 없습니다.

    fun <T : Number> List<T>.sum() : T
    //상한타입을 Number으로 지정합니다.

     

    타입 파라미터를 널이 될 수 없는 타입으로 한정

    만약 상한 타입을 지정하지 않는다면 Any?를 상한으로 정한 파라미터와 같아집니다.

     

    따라서 Null이 될 수 있는 파라미터가 돼버립니다.

     

    이를 방지하기 위해서는 Any로 상한 타입을 한정시킵니다.

     

    제네릭스 타입 소거

    JVM의 제네릭스는 타입 소거를 사용해 구현됩니다.

    이는 실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않다는 뜻입니다.

     

    List<String> 객체를 만들고 그 안에 문자열을 여러 넣더라도 실행 시점에는 그 객체를 오직 List로만 봅니다.

    즉, List 객체가 어떤 타입의 원소를 저장하는지 실행 시점에는 알 수 없습니다.

     

    이로 인해 실행 시점에 타입 인자를 검사할 수 없습니다.

    if (value is List<String> {...}
    ERORR : Cannot check for instance of erased type

    이는 저장해야 하는 타입 정보의 크기가 줄어서 전반적인 메모리 사용량이 줄어든다는 장점이 있습니다.

     

    as난 as? 캐스팅에도 여전히 제네릭 타입을 사용할 수 있습니다.

    하지만 기저 클래스는 같지만 타입 인자가 다른 타입으로 캐스팅해도 여전히 캐스팅에 성공한다는 점을 조심해야 합니다.

     

    fun printSum(c: Collection<*>{
    	val intList = c as? List<Int> //Unchecked cast 경고 발생
    		?: throw IllegalArgumentException()
    	println(intList.sum())
    }
    
    printSum(listOf(1,2,3,))

    컴파일러가 캐스팅 관련 경고를 한다는 점을 제외하면 모든 코드가 문제없이 컴파일됩니다.

     

    하지만 이후에 잘못된 타입의 원소가 들어있는 리스트를 전달하면 실행 시점에 ClassCastException이 발생합니다.

     

    하지만 inline 함수 안에서는 타입 인자를 사용할 수 있습니다.

     

    인라인 함수의 타입 파라미터는 실체화되므로 실행 시점에 인라인 함수의 타입 인자를 알 수 있습니다.

     

    인라인 함수에서만 실체화한 타입 인자를 쓸 수 있는 이유

    컴파일러는 인라인 함수의 본문을 구현한 바이트코드를 그 함수가 호출되는 모든 지점에 삽입합니다.

    이때 각 부분의 정확한 타입 인자를 알 수 있습니다.

     

    변성 : 제네릭과 하위 타입

    변성 개념은 List<String>과 List<Any>와 같이 기저 타입이 같고 타입 인자가 다른 여러 타입이 서로 어떤 관계가 있는지 설명하는 개념입니다.

     

    직접 제네릭 클래스나 함수를 정의하는 경우 변성을 꼭 이해해야 합니다.

     

    변성을 잘 활용하면 사용 시 불편하지 않으면서 타입 안전성을 보장하는 API를 만들 수 있습니다.

     

    변성이 있는 이유

    List<Any> 타입의 파라미터는 받는 함수에 List<String>을 넘기면 안전할까요?

    String은 Any를 확장하기 때문에 절대로 안전해 보입니다.

     

    하지만 어떤 함수가 리스트의 원소를 추가하거나 변경한다면 타입 불일치가 생길 수 있어서 List<Any> 대신 List<String>을 넘길 수 없습니다.

     

    fun addAnswer(list: MutableList<Any>){
    	list.add(42)
    }

    위의 list로 List<String>이 들어온다면? String을 받아야 하는데 Int를 add 하는 순간 예외가 발생합니다.

     

    하위 타입

    하위 타입은 어떤 타입 A의 값이 필요한 모든 장소에 어떤 타입 B의 값을 넣어도 아무 문제가 없다면 타입 B는 타입 A의 하위 타입입니다.

     

    예를 들어 Int는 Number의 하위 타입이지만 String은 아닙니다.

     

    무공변

    제네릭 타입을 인스턴스화할 때 타입 인자로 서로 다른 타입이 들어가면 인스턴스 타입 사이의 하위 타입 관계가 성립하지 않으면 그 제네릭 타입을 무공변이라고 말합니다.

     

    자바에서는 모든 클래스가 무공변입니다.

     

    하지만 코틀린은 읽기 전용 컬렉션을 제공하고 A가 B의 하위 타입이라면 공변적이라고 할 수 있습니다.

     

    공변성

    제네릭 클래스 타입 파라미터에 대해 공변적임을 표시하려면 타입 파라미터 앞에 out을 넣어야 합니다.

    interface Producer<out T>{
    	fun produce() : T
    }

    함수 정의 사용한 파라미터 타입과 타입 인자의 타입이 정확히 일치하지 않아도 그 클래스의 인스턴스를 함수 인자라 반환 값으로 사용할 수 있습니다.

     

    반공변성

    공변성의 반대로 - 자기 자신과 부모 객체만 허용합니다 Java에서의 <? suepr T>와 같습니다.

    Kotline에서는 in 키워드를 사용해서 표현합니다.

    interface Consumer<in T>{
    	fun consume() : T
    }

     

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

    제네릭 타입 인자 정보가 없음을 표현하기 위해 스타 프로젝션을 사용합니다.

    MutableList<*>는 MutableList<Any?>와 같지 않습니다.

    Any는 모든 타입을 담을 수 있음을 의미하지만 *은 어떤 정해진 구체적인 타입 원소만을 담지만 그 타입을 정확히 모른다는 사실을 표현합니다.

     

    728x90

    댓글

Designed by Tistory.