4. 명시적 타입 캐스팅
명시적 타입캐스팅은 컴파일러가 타입을 확실하게 결정할 수 없어 스마트 캐스팅을 하지 못할 경우에만 사용한다.
예를 들면 var 변수가 체크와 사용 사이에서 변경되었다면 코틀린은 타입을 보장해줄 수 없다.
(val 은 이뮤터블, var 는 뮤터블)
코틀린은 명시적 타입 캐스트를 위해서 2가지 연산자를 제공한다. as와 as? 이다.
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> 가 있다고 한다.
'이전 프로젝트 > Kotlin' 카테고리의 다른 글
[Kotlin] 6-1. 오류를 예방하는 타입 안정성 (0) | 2022.02.21 |
---|---|
[Kotlin] 번외 1. lotto 생성기 (0) | 2022.02.14 |
[Kotlin] 5. 콜렉션 사용하기 (0) | 2022.02.13 |
[Kotlin] 4. 외부 반복과 아규먼트 매칭 (0) | 2022.02.12 |
[Kotlin] 3. 함수를 사용하자 (0) | 2022.02.02 |