Today I Learned

오류를 예방하는 타입 안정성 (2) 본문

Kotlin/다재다능 코틀린 프로그래밍

오류를 예방하는 타입 안정성 (2)

하이라이터 2023. 8. 1. 16:24
728x90

제네릭 : 파라미터 타입의 가변성과 제약사항

  • 제네릭을 사용하면 다양한 타입에서 사용 가능한 코드를 만들 수 있다.
  • Java의 제네릭
    • 타입 불변성
    • <? extends T>와 <? super T> 문법을 사용해서 파라미터 타입 T의 자식 클래스와 부모 클래스도 사용할 수 있다.
    • 하지만 이 문법은 제네릭을 사용할 땐 사용 가능하고(사용처 가변성), 제네릭을 선언할 때는 사용이 불가능하다(선언처 가변성).

 

타입 불변성

  • 메소드가 타입 T의 제네릭 오브젝트를 받는다면(ex. List<T>), T의 파생클래스(부모, 자식)를 전달할 수 없다.
open class Fruits
class Banana : Fruit()
class Orange : Fruit()

fun receiveFruits(friuts: Array<Fruit>) {
  println("Number of fruits: ${fruits.size}")
}

val bananas: Array<Banana> = arrayOf()
receiveFruits(bananas) //ERROR: type mismatch
  • Array<Banana>가 Array<Fruit>을 인자로 받는 메소드에 인자로 전달될  수 있다면 Orange를 Array<Fruit>에 담게 될 때 문제가 발생한다.
  • 이번에는 Array<Fruit> 대신에 List<Fruit>으로 파라미터 타입을 변경해보자.
fun receiveFruits(friuts: List<Fruit>) {
  println("Number of fruits: ${fruits.size}")
}

val bananas: List<Banana> = listOf()
receiveFruits(bananas) //OK
  • List<T>는 이뮤터블하기 때문에 Orange를 List<Fruit>에 추가할 수 없다. 따라서 에러가 발생하지 않는다.

 

공변성 사용하기

fun copyFromTo(from: Array<Fruit>, to: Array<Fruit>) {
  for (i in 0 until from.size) {
    to[i] = from[i]
  }
}

val fruitsBasket = Array<Fruit>(3) { _ -> Fruit() }
val bananaBasket = Array<Banana>(3) { _ -> Banana() }
copyFromTo(bananaBasket, fruitsBasket) // ERROR: typeMismatch
  • Banana가 아닌 Fruit을 Array<Banana>에 추가하면 안되기 때문에 에러가 발생한다.
fun copyFromTo(from: Array<out Fruit>, to: Array<Fruit>) {
  for (i in 0 until from.size) {
    to[i] = from[i]
  }
}

copyFromTo(bananaBasket, fruitsBasket) // OK
  • from 파라미터는 파라미터의 값을 읽기만 하기 때문에 Array<T>의 T에 Fruit 클래스나 하위 클래스가 전달되더라도 위험이 없다.
    이것을 타입이나 파생 타입에 접근하기 위한 파라미터 타입의 공변성이라고 한다.
  • T의 자식 클래스들을 전달 가능하게 만들기 위하여 from: Array<out T> 문법을 사용할 수 있다.
  • Array<T> 클래스는 T 타입의 객체를 읽고, 쓰는 메소드를 모두 가지고 있지만, 공변성을 사용하기 위해서 Array<T> 파라미터에서 어떤 값도 추가하거나 변경하지 않겠다는 약속을 해야 한다.

선언처 가변성: 선언하는 시점에 공변성을 사용하겠다고 지정(ex. List<out T>)

사용처 가변성: 사용하시는 시점에 공변성을 사용하겠다고 지정 = 타입 프로젝션 (ex. Array<out T>)

 

반공변성 사용하기

val things = Array<Any>(3) { _ -> Fruit() }
val bananaBasket = Array<Banana>(3) { _ -> Banana() }
copyFromTo(bananaBasket, things) //Error:type mismatch
  • to 파라미터를 유연하게 사용하고자 Array<Any> 타입의 파라미터를 넣으면 에러가 발생한다.
fun copyFromTo(from: Array<out Fruit>, to: Array<in Fruit>) {
  for (i in 0 until from.size) {
    to[i] = from[i]
  }
}

copyFromTo(bananaBasket, things) // OK
  • to 파라미터를 Array<in T> 형식으로 변경하면 에러가 발생하지 않는다.
  • in 키워드는 메소드가 파라미터에 값을 설정할 수 있게 만들고, 값을 읽을 수 없게 만든다.(반공변성)
  • <in T>로 정의되면 전체적으로 파라미터 타입을 받을 수만 있고 리턴하거나 다른 곳으로 보낼 수는 없는 반공변성으로 특정된다.

 

where를 사용한 파라미터 타입 제한

  • 제네릭을 사용할 때 제약조건이 필요한 경우도 있다.
fun <T> useAndClose(input: T){
  input.close() //ERROR: unresolved reference: close
}
  • 위의 예제에서 타입 T는 close() 메소드를 서포트해야하지만, 어떤 타입은 close() 메소드가 없다.
  • 인터페이스를 통해서 close() 메소드만 들어올 수 있도록 제약을 걸 수 있다.
fun <T: AutoCloseable> useAndClose(input: T){
  input.close()
}
  • 하지만 여러 개의 제약 조건을 넣을 때에는 위 방식이 불가능하다. 이 경우는 where을 이용할 수 있다.
fun <T>useAndClose(input: T)
where T: AutoCloseable,
T: Appendable {
  input.append("there")
  input.close()
}

 

스타 프로젝션

  • 스타 프로젝션<*>은 제네릭 읽기전용 타입과 raw 타입을 위한 코틀린의 기능이다.
  • 타입에 대해 정확히는 알 수 없지만 타입 안정성을 유지하면서 파라미터를 전달할 때 사용한다.
fun printValues(values: Array<*>) {
  for (value in values) {
    println(value)
  }
  //values[0] = values[1] //ERROR
}
printValues(arrayOf(1,2)) //1\n2

 

구체화된 타입 파라미터

abstract class Book(val name: String)
class Fiction(name: String) : Book(name)
class NonFiction(name: String) : Book(name)

val books: List<Book> = listOf(
  Fiction("Moby Dick"), NonFiction("Lean to Code"), Fiction("LOTR"))
  • 위 코드에서 List<Book>에서는 Fiction과 NonFiction이 섞여있다.
  • 이제 list 안의 Fiction과 NonFiction 중 특정 타입의 첫 번째 인스턴스를 찾아야 한다고 생각해보자.
fun <T> findFirst(books: List<Book>, ofClass: Class<T>): T {
  val selected = books.filter {book -> ofClass.isInstance(book) }
  if(selected.size == 0) {
    throw runTimeException("Not found")
  }
  return ofClass.cast(selected[0])
}

println(findFirst(book, NonFiction::class.java).name) //Learn to Code
  • 바이트코드로 컴파일되면서 파라미터 타입 T가 지워지기 때문에 함수 안에서 T를 book is T나 selected[0] as T 처럼 연산자와 함께 사용할 수 없다.
  • 따라서 원하는 객체 타입을 파라미터로 던지고 ofClass로 타입 체크와 타입 캐스팅을 해야한다.
  • 하지만 코틀린에서는 reified(구체화) 타입 파라미터를 사용할 수 있다.
inline fun <reified T> findFirst<books: List<Book>): T {
  val selected = books.filter {book -> book is T }
  if(selected.size == 0) {
    throw runTimeException("Not found")
  }
  return selected[0] as T
}
  • inline 함수는 컴파일 시점에 확정되므로 함수 호출 시에 오버헤드가 없는 함수이다. 자세한건 추후에 다시 다루겠다.
  • 타입 T를 reified로 선언하면 함수 안에서 T를 타입 체크와 캐스팅 용으로 사용 가능하다.
  • Reified 타입 파라미터는 함수에 추가적인 클래스 정보를 전달하지 않도록 만들어주고, 안전하게 캐스팅하는데 도움을 주며 컴파일 시간 안정성을 확보한 채로 리턴타입을 커스터마이징 할 수있게 해준다.
728x90

'Kotlin > 다재다능 코틀린 프로그래밍' 카테고리의 다른 글

클래스 계층과 상속  (0) 2023.08.24
객체와 클래스  (0) 2023.08.01
오류를 예방하는 타입 안정성(1)  (0) 2023.07.27
콜렉션 사용하기  (0) 2023.07.27
외부 반복과 아큐먼트 매칭  (0) 2023.07.21
Comments