코틀린 인 액션 9장

존스노우·2023년 4월 15일
0

코틀린

목록 보기
9/10

제네릭스

  • 실체화한 파라미터 , 선언지점 변성에 대해 소개
  • 실체화한 타입 파라미터 사용? 타입 인자로 쓰인 구체적인 타입을 실행 시점에 알 수 있다?

요약

  • 제네릭 타입 사용시 타입을 일반화되어 재사용성을 높이지만
  • 사용시점에서 구체적인 타입을 알수 없다.
  • 이 때 실체화한 타입 파라미터를 사용해 실행 시점 타입 인자의 구체적인 타입을 알 수 있다.
  • 선언 지점 변경을 사용하면 타입 인자의 상위/하위 타입을 지정 가능하다 자바에선 ? 임
   ```
   // reified 예약어를 사용해 실체화한 파라미터 선언.
    inline fun <reified T> printType() {
        println(T::class.simpleName)
    }

    fun main() {
        printType<Int>() // Int
        printType<String>() // String
        printType<List<Boolean>>() // List
    }


// 자바 와일드카드를 이용한 사용자 지정 변성
// Number 클래스의 하위 클래스 타입인 Integer와 Double을 모두 인자로 전달 가능
import java.util.*;

public class Example {
    public static void main(String[] args) {
        List<Integer> integers = Arrays.asList(1, 2, 3);
        List<Double> doubles = Arrays.asList(1.0, 2.0, 3.0);

        printList(integers);
        printList(doubles);
    }

    public static void printList(List<? extends Number> list) {
        for (Number n : list) {
            System.out.println(n);
        }
    }
}

// 코틀린 선언 지점 변성 을 사용한 타입 인자 제한.
//opy 함수는 from 파라미터가 T 타입의 하위 타입을 받도록 out 키워드를 사용하여 선언
   fun <T> copy(from: MutableList<out T>, to: MutableList<T>) {
    for (item in from) {
        to.add(item)
    }
}

fun main() {
    val dogs: MutableList<Dog> = mutableListOf(Dog("Rex"), Dog("Fido"))
    val animals: MutableList<Animal> = mutableListOf()

    copy(dogs, animals)
    println(animals) // Output: [Dog(name=Rex), Dog(name=Fido)]
}


// out 키워드를 사용한 예시 코드
  interface AnimalShelter<out T> {
      fun adoptAnimal(): T?
  }

class DogShelter : AnimalShelter<Dog> {
    private val dogs: MutableList<Dog> = mutableListOf()

    override fun adoptAnimal(): Dog? {
        if (dogs.isNotEmpty()) {
            return dogs.removeAt(0)
        }
        return null
    }
}

class Animal(val name: String)

open class Dog(name: String) : Animal(name)

class Cat(name: String) : Animal(name)

fun main() {
    val dogShelter: AnimalShelter<Dog> = DogShelter()
    val animalShelter: AnimalShelter<Animal> = dogShelter

    println(animalShelter.adoptAnimal()?.name) // Output: Rex
}

  ```

왜 사용?

  • 일반적으로 제네릭 타입은 컴파일 시점에서 타입 소거가 일어나.
    실행시점에 제네릭타입 정보를 알수 없음, 그래서 실체화한 타입파라미터 사용.
    타입 정보유지.

  • 선언 지점 변성으로 타입인자 제한해서.. 특정 인자만 사용 가능하도록 제한.
    코드 안정성 높임

  • 코드 재사용성 증대

언제 사용 할까??

  • 주로 인라인 함수에 실체화한 타입 파라미터 사용
  • 람다 표현식이나 함수의 타입인자 사용하는 경우,
  • 만약 사용을 안하면? 타입 소거를 수행하므로 컴파일 시점에서 타입을 알 수 없어서..
  • 수동으로 캐스팅 해줘야됨,,

제네릭 타입 파라미터 .

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

  • 코틀린 컴파일러는 보통 타입 인자 추론 가능 .

  • 코틀린에서 제네릭 사용시 타입추론이나 타입을 명시해 줘야됨

  • 왜? 타입의 안정성 보장 및 raw 타입 지원하지 않아서.

    // List<T> 제네릭 타입을 사용한 예시
    List intList = Arrays.asList(1, 2, 3);
    List stringList = Arrays.asList("a", "b", "c");
    
    // Map<K, V> 제네릭 타입을 사용한 예시
    Map intToStringMap = new HashMap();
    intToStringMap.put(1, "one");
    intToStringMap.put(2, "two");
    intToStringMap.put(3, "three");
    
    Map stringToIntMap = new HashMap();
    stringToIntMap.put("one", 1);
    stringToIntMap.put("two", 2);
    stringToIntMap.put("three", 3);
  • 자바에서 예시 자바는 raw타입 지원해서 타입을 따로 생략해도 된다.

  • Raw 타입은 제네릭 타입에서 타입 인자를 생략한 것을 의미

  • 원래는 제네릭이 없었기 때문에 타입인자가 없는 제네릭 사용하려면 Raw타입이 필요했다.

제네릭 함수와 프로퍼티

  • 모든 리스트(제네릭 리스트)를 다룰 함수도 다루길 원함!?
  • 코틀린에서 제네릭함수 호출시 반드시 타입 명시.
  • fun List.slice(indices: IntRange): List
  • 수신객체와 반환타입을 List로 가지고 있는 함수.
  • 결론은 코틀린은 타입추론 및 타입을 지정해줌으로서 타입의 안정성을 제공한다는 의미..같은데.

제네릭스 클래스 선언

  • 그냥 저냥.. 딱히

타입 파라미터 제약

  • 크래스나 타입 인자를 제한 기능.
  • fun <T: Number> sum(numbers: List): T
  • Number라는 타입 상한선을 둠.
  • : 은 extend 처럼 쓰임..
  • 파라미터 두개 타입 제한가능하다 뭐 이런얘기.

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

  • 함수를 인라인을 ㅗ만들면 타입 인자가 지워지지않음 ( 코틀린에서 실체화라고부름)

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

  • 코틀린 제네릭 타입 인자 정보는 런타임에 지워짐.
  • 제네릭 클래스 인스턴스가 생성할때 쓰인 타입 인자에 대한 정보를 유지하지 않는다는 뜻

  • 실행 시점에서 둘은 같은 타입이지만 컴파일러가 타입인자는 타입인자를 알아서
  • 올바른 타입값만 넣어주도록 보장을 해준다.
  • 허나 타입 소거 때문에 실행 시점에 타입 인자 검사는 안돼
  • 허나 저장 해야되는 타입정보가 줄어들어 메모리 사용량은 줄어 든다.

허나 타입인 자를 지정하지않고 사용할수 없는 코틀린제네릭

  • 그러면 어떻게? 어떤 값이집합이나 맵이 아니라 리스트라는 사실을 확인?

스타 프로젝션

  • if(vlaue is List<*>)

  • 타입 파라미터가 2개 이상이라면 모든 타입 파라미터에 * 포함 시켜야 한다.

  • 인자를 알 수 없는 제네릭 타입으로 표현할때 (자바에선 ? )

  • 쓴다..

번외 스타프로젝션에 대해

  • 와일드 카드를 대체하는 코틀린의 기능 ?

  • 알수 없는 유형 매개변수가 있는 일반 유형 처리할때 유용

    fun printContents(box: Box<*>) {
       val contents = box.contents
       println("Box contains $contents")
    }
    
    		val list: List<*> = listOf("Hello", 42, true)
    
  • 와닿지는 않지만 모든 유형을 유연하게 사용가능하는거같다.

    fun printSum(c: Collection<*>) {
        val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected")
        println(intList.sum())
    }
    
    printSum(listOf(1, 2, 3)) // prints 6
      printSum(setOf(1, 2, 3))  // throw error
      printSum(listOf("a", "b", "c")) //type cast error
    
  • 이런식으로 예제..

  • 뭐지 ??

  • 타입소거때문에 두번째는 예외가 발생. 실행시점에 타입이 지워졌기때문. 그래서 as는 위험.

  • as를 사용할땐 조심해야 한다. 잘못된 유형으로 캐스트 할 가능성이 있따.

    fun printSum (c: Collection<Int>) (
    if (c is List<Int>) { printin (c.sum ())
    )
    
  • Is 예약어는 컴파일 시점에 타입이 뭔지 명시한다.

  • is 연산자는 컴파일 시간에 유형 검사를 수행하는 데 사용됨

  • 일반적으로 is 연산자를 사용하여 유형 검사를 수행하는 것이 더 안전

  • 변수나 표현식을 특정 유형으로 캐스트해야 하고 캐스트가 안전하다고 확신하는 경우 'as' 연산자를 사용

  • 아 다시 풀어서 정리하면 Is예약어는 타입을 확인할때 as는 그 타입이 확실하다할때 쓴다.

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

  • fun isA(value: Any) = value is T

  • 위 식처럼 제네릭 함수의 타입 인자도 호출시 타입 인자를 알수 없다.

  • 제약을 피하려면?

  • 여기서 다시 Inline 키워드를 붙이면 컴파일러는 함수를 호출한 식을 함수 본문으로 바꿈.

  • 함수가 람다를 인자로 사용하는 경우 그 함수를 인라인으로 만들면?

  • 뭐 성능좋은얘기만 나오고..

    inline fun <reified T> isA(value: Any) = value is T
    
    println(isA<String>("abc")) // Output: true
    println(isA<String>(123))   // Output: false.
  • refied 예약 어는 런타임시 유형 매개변수의 런타임 시 유형 매개변수의 유형 정보를 보존하는 데 사용

  • 매개변수가 사용되는 함수 또는 클래스의 범위 내에서 런타임 시 매개변수의 유형 정보를 보존

  • 유형 정보를 보존하기위해 인라인을 해야되는 작업이 필요하다.

  • 인라인으로 표시하면 컴파일러는 유형 매개변수가 런타임시 유형 정보 보존하도록 추가 바이트코드 생성하기 때문에

  • reified” 키워드는이타입파라미터가 실행시점에 지워지지 않음을 표시한다. (책 참조)

val items = listOf("one", 2, "three")
println(items.filterIsInstance<String>()) // Output: [one, three]
  • 실체화한 파라미터 사용 예시 실체화한 파라미터란 제네릭시점에서 타입을지정해주면
  • 제네릭이 선언될 때, 제네릭 클래스의 인스턴스를 생성하거나 제네릭 메서드를 사용할 때 특정 타입을 제공하면, 그 특정 타입을 인스턴스화된 타입 매개 변수
  • 내가 이해한 바로는 이럼.
  • ArrayList라는 Java의 일반 클래스를 고려할때 String(특정유형) 사용하면
  • 인스턴스를 만들때 ArrayList 을 작성함. 이경우 String은 구체화된 매개변수
 inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
     val destination = mutableListOf<T>()
     for (element in this) {
         if (element is T) {
             destination.add(element)
         }
     }
     return destination
 }

위 정리한 내용에 참고할만한 글

  • 인라인 함수에서만 인스턴스화된 유형 인수를 사용할 수 있는 이유는 타입 소거 때문
  • 제네릭을 지원하지 않는 이전 버전과 호환때문에 타입 소거를 한다.
  • 인라인 함수를 사용하면 컴파일시 해당 유형 인자까지 같이 본문에 출력되니까 (바이트코드를 추가작성)
    그래서 인라인화된 함수에만 실체화된 인스턴스를 쓸수 있구나

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

  • val serviceImpl = ServiceLoader. load (Service: :class.java)
  • val serviceImol = loadservice()

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

  • 왜 실체화된 파라미터를 사용하지.

  • 인스턴스화된 유형 매개변수를 사용하여 유형 안전성을 강화하고 코드 재사용성을 촉진하며 가독성을 높이고 Java와의 상호 운용성을 유지

  • 사용할 수 있는경우

  • 타입 검사 캐스팅, 리플렉션, 코틀린 타입에 대응하는 java.lang.class얻기, 다른함수 호출할때 타입 인자 사용등.. ?

    class Example {
       companion object {
           fun <T> create(): T {
               // This will not compile because we cannot create an instance of a type parameter directly
               // return T()
    
               // Instead, we can use reflection to create an instance of the type parameter class
               @Suppress("UNCHECKED_CAST")
               val clazz = T::class.java as Class<T>
               return clazz.newInstance()
           }
       }
    
       inline fun <reified T> callFunction() {
           // We can pass the non-materialized type parameter T to a function that requires a materialized type parameter
           val instance = Example.create<T>()
           println(instance)
    
           // We can also call a companion object method of a type parameter class
           val result = T.someCompanionObjectMethod()
           println(result)
       }
    }
    
    class MyClass {
       companion object {
           fun someCompanionObjectMethod(): String {
               return "Hello from companion object method!"
           }
       }
    }
    
    fun main() {
       val example = Example()
       example.callFunction<MyClass>()
    }
    

변성 관련

  • 제네릭 타입을 인스턴스화 할때 타입인자로 서로 다른 타입이 들어가면
  • 인스턴스 사이의 하위 타입관계가 성립하지 않을때 그 제네릭 타입을 무공변(일관되지않는)이라 함.
  • A 가 B의 하위타입이면 List 는 List 의 하위타입 공변적이라함.
interface Producer<out T> {
    fun produce(): T
}

class StringProducer : Producer<String> {
    override fun produce(): String {
        return "Hello, world!"
    }
}
  • out 예약어를 사용함으로서 타입이 공변적임을 표시. 없으면 무공변성으로 표시.

  • 공변 유형 매개변수를 선언하는 데 사용

  • out 선언이 없으면 컴파일러가 T가 누구의 하위 클래스인지 여부를 알 수 없기 때문

  • 그러나 읽기는 가능하나 쓰기 변경은 안됌

    class Herd<out T : Animal> {
       // T is now covariant.
       fun takeCareOfCats(cats: Herd<Cat>) {
           for (i in 0 until cats.size) {
               cats[i].cleanLitter()
               feedAll(cats)
           }
       }
       //...
    }
    
    // Error: inferred type is Herd<Cat>, but Herd<Animal> was expected.
    // fun feedAll(animals: Herd<Animal>) {
    // ...
    // }
    
    // The Herd class provides an API similar to List, and you cannot add animals to the class or change animals in the herd to other animals.
    // So, make Herd a covariant class, and you can change the calling code appropriately.
    fun feedAll(animals: Herd<out Animal>) {
       // 
    }

  • in 위치와 out 위치

  • in은 소비 out은 생산.

  • (함수의반환타입>는아웃위치에서만 사용가능 (out)

  • List는 읽기 전용 타입을 추가하거나 기존 값 변경메서드가없다

  • 이는 List는 T에 대해 공변적이다 라고 한다 책에서.

  • MutableList를 타입파라미터에 대해 공변적인 클래스로 선언할수는 없 다 는 점 유의.

  • class Herd<T: Animal> (var leadAnimal: T, vararg animals: T) (. . . )

  • leadAnimal 프로퍼티가인위치에있기때문에꼬를out으로표시할 수 없다.

  • 변성규칙은클래스외부의사용자가클래스를잘못사용하는일을막기
    위한것이므로클래스내부구현에는적용되지않는다.

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

  • in 이라는 키워드는 그 키워드가 붙은 타입을! 클래스의 메소드 안으로
  • 전달돼 메소드에 의해 소비된다는 뜻!
  • 공변성경우와 마찬가지로 타입 파라미터 사용을 제한한다는 뜻. 특정 하위 타입관계에 도달 가능.

	// 사용 지점 변성예 out 키워드를 사용함으로서 해당 타입은 in 위치에 사용하지 않는다 . 
//이때 타입프로젝션이 일어나, source는 일반적인 mutableList가 아니라
// MutableList를 프로젝션한 (제약을가한 ) 타입이 됨. 
// 결론적으로 타입파라미터를 아웃위치에서 사용하는 메소드만 호출할수있다
fun <T> copyData(source: MutableList<out T>, destination: MutableList<T>) {
    for (item in source) {
        destination.add(item)
    }
}


  • List 경우 기본적으로 List

///

  • 사용 지점 변성 : 사용하는 위치에 파라미터에 대한 제약 명시

  • 선언 지점: 기저 객체에 사용때 지정 ..

  • super type token 조사

  • 제네릭을 쓸때 슈퍼타입토큰을 쓴다? 그래서 타입이 지정된다.

profile
어제의 나보다 한걸음 더

0개의 댓글