Kotlin

 

4. 명시적 타입 캐스팅

명시적 타입캐스팅은 컴파일러가 타입을 확실하게 결정할 수 없어 스마트 캐스팅을 하지 못할 경우에만 사용한다.

예를 들면 var 변수가 체크와 사용 사이에서 변경되었다면 코틀린은 타입을 보장해줄 수 없다.

(val 은 이뮤터블, var 는 뮤터블)

 

코틀린은 명시적 타입 캐스트를 위해서 2가지 연산자를 제공한다. asas? 이다.

 

fun fetchMessage(id: Int): Any = if (id == 1) "Record found" else StringBuilder("data not found")

for (id in 1..2) {
    println("Message length: ${(fetchMessage(id) as? String)?.length ?: "---"}")
}

 

  • 가능한 스마트 캐스트를 사용하기 (Any와 is 타입을 활용하자)
  • 스마트 캐스트가 불가능한 경우에만 안전한 캐스트 연산자를 사용하기 (as? 와 ? 세이프 콜, 엘비스 연산자)
  • 안전하지 않은 캐스트 연산자는 사용하지 않기 (확정 연산자 !! 쓰지 않기)

 

아래에 나오는 공변성과 반공변성을 조금 어렵다고 하니 ⚠️ 주의 ⚠️

 

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

자바에서의 <? extends T> 문법을 사용해서 공변성(convariance) 을 사용했다.

 

자바 제네릭은
extends T : 상한 경계 (~Number 까지)
? super T : 하한 경계 (Integer 부터~)

이렇게 사용한다.

 

자바에서는 제네릭을 사용할 땐 사용이 가능하지만, 제네릭을 선언할땐 사용이 불가능한데,

코틀린에서는 두 가지 경우에서도 모두 사용이 가능하다고 한다.

 

5.1 타입 불변성

 

open class Fruit
class Banana : Fruit()
class Orange: Fruit()

 

이러한 코드가 있을 때

 

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

 

에서는 에러가 나지만 Array 를 List 로 바꾸면 에러가 나지 않는다.

 

open class Fruit
class Banana : Fruit()
class Orange: Fruit()

/*
fun receiveFruits(fruits: Array<Fruit>) {
    println("number of fruits: ${fruits.size}")
}

val bananas: Array<Banana> = arrayOf()
receiveFruits(bananas) // ERROR: type mismatch
*/

fun receiveFruits(fruits: List<Fruit>) {
    println("number of fruits: ${fruits.size}")
}

val bananas: List<Banana> = listOf()
receiveFruits(bananas)

 

Array<T> 는 뮤터블하지만 List<T>는 이뮤터블 하기에 개발자가 Orange는 Array<Fruit>에 추가할 수 있지만 List<Fruit> 에는 추가할 수 없다고 한다.

 

Array<T> 는 class Array<T> 로 정의되며 List<T>는 interface List<out E> 로 정의한다.

가장 큰 차이는 out 에 달려있다.

 

5.2 공변성 사용하기

위에서 봤듯이, 코틀린은 Array<Banana> 가 Array<Fruit> 을 받아야 하는 곳에 전달되는 것을 막는다.

그리고 이상한 과일이 Banana 배열에 추가되는 걸 막아준다.

 

하지만 가끔 공변성을 허용하길 원할 때, 타입 프로젝션이 필요하다.

 

open class Fruit
class Banana : Fruit()
class Orange: Fruit()

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

/*
val fruitsBasket1 = Array<Fruit>(3) { _ -> Fruit() }
val fruitsBasket2 = Array<Fruit>(3) { _ -> Fruit() }
copyFromTo(fruitsBasket1, fruitsBasket2); // 정상 동작
*/

val fruitsBasket = Array<Fruit>(3) { _ -> Fruit() }
val bananaBasket = Array<Banana>(3) { _ -> Banana() }
copyFromTo(bananaBasket, fruitsBasket); // ERROR: type mismatch

 

이 코드를 동작하게 고치면

 

open class Fruit
class Banana : Fruit()

fun copyFromTo(from: Array<out 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);

 

Array<out Fruit> 는 코틀린에게 Array<out T> 의 공변성 파라미터에 변경이나 추가가 없다는 것을 보장해준다.

공변성을 사용하면 컴파일러에게 자식 클래스를 부모 클래스의 자리에 사용할 수 있게 요청할 수 있다.

 

 

5.3 반공변성 사용하기

 

open class Fruit
class Banana : Fruit()

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

val thing = Array<Any>(3) { _ -> Fruit() }
val bananaBasket = Array<Banana>(3) { _ -> Banana() }
copyFromTo(bananaBasket, thing);

 

반공변성에서는 in 을 사용한다.

 

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

제네릭은 파라미터에 여러 타입을 쓸 수 있도록 유연함을 주지만 가끔 제약조건을 줄 때가 필요하다.

 

fun <T> useAndClose(input: T) {
    input.close() // ERROR: unresolved reference: close
}

 

이때 input에 close 메소드가 무조건 있을 순 없기 때문에 제약조건이 필요한데, 그 전에 AutoCloseable 인터페이스를 써보자.

 

fun <T: AutoCloseable> useAndClose(input: T) {
    input.close() // OK
}

val writer = java.io.StringWriter()
writer.append("hello ")
useAndClose(writer)
println(writer) // hello

 

하지만 모든 타입이 되는 게 아니고, AutoCloseable 인터페이스를 구현한 클래스만 가능하며

여러 개의 제약 조건을 넣을 땐 where 를 사용해야 한다.

 

fun <T> useAndClose(input: T)
where T: AutoCloseable,
T: Appendable {
    input.append("there")
    input.close()
}

val writer = java.io.StringWriter()
writer.append("hello ")
useAndClose(writer)
println(writer) // hello there

 

AutoCloseable, Appendable 두 제약조건을 만족하는 클래스의 인스턴스라면 어떤 인스턴스도 전달할 수 있다.

 

5.5 스타프로젝션

스타프로젝션 <*> 은 제네릭 읽기전용 타입과 raw 타입을 위한 코틀린의 기능이다.

타입에 대해 모르지만 안정성을 유지하면서 파라미터를 전달할 때 사용된다.

 

fun printValues(values: Array<*>) {
    for (value in values) {
        println(value)
    }

    //values[0] = values[1]
}

printValues(arrayOf(1, 2)) //1\n2

 

6. 구체화된 타입 파라미터

코틀린은 구체화된 타입 파라미터 (Reified Type Parameters) 를 이용해서 특정 타입의 인스턴스를 출력하는 코드를 만들어보자

 

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("Learn to Code"), Fiction("LOTR"))

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(books, NonFiction::class.java).name) // Learn to Code

 

위 코드는 reified로 리팩토링하기 전 코드이고

reified 를 적용하면 다음과 같이 나온다.

 

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("Learn to Code"), Fiction("LOTR"))

/*
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(books, NonFiction::class.java).name) // Learn to Code
*/

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
}

println(findFirst<NonFiction>(books).name) // Learn to Code

 

inline 의 장점은 다음 시간에 알려준다고 한다 (10장에서 ㅠ)

파라미터 타입 T를 reified 로 선언하고 Class<T> 파라미터를 제거했다.

함수 안에서 T를 타입 체크캐스팅 용으로 사용 가능하다.

 

좀 더 가독성이 올라가는 것 같다.

 

reified 타입 파라미터를 사용하는 함수는 대표적으로 코틀린 스탠다드 라이브러리의 listOf<T>() 와 mutableListOf<T>() 함수,

klaxon 라이브러리에 있는 parse<T> 가 있다고 한다.