is and !is operators
객체가 주어진 Type을 준수하는지 여부를 식별하는 런타임에 타입 체크를 수행하려면 is 연산자 또는 부정 형식인 !is 를 사용하라.
if (obj is String) {
print(obj.length)
}
if (obj !is String) { // == !(obj is String)
print("Not a String")
} else {
print(obj.length)
}
Smart casts
대부분의 경우, 코틀린 컴파일러는 변경 불가능한 값에 대해 is-check 및 명시적 캐스트를 추적하고 필요할 때 자동으로 (안전한) 형변환을 삽입하기 때문에 명시적 캐스트 연산자를 사용할 필요가 없다.
fun demo(x: Any) {
if (x is String) {
print(x.length) // x is automatically cast to String
}
}
코틀린 컴파일러는 !is 연산의 결과로 return이 호출되는 겨우 캐스팅이 안전한다고 판단한다.
if (x !is String) return
print(x.length) // x is automatically cast to String
&&나 || 연산자의 오른쪽 피연산자에서도 자동으로 캐스팅 해준다.
// x is automatically cast to String on the right-hand side of `||`
if (x !is String || x.length == 0) return
// x is automatically cast to String on the right-hand side of `&&`
if (x is String && x.length > 0) {
print(x.length) // x is automatically cast to String
}
smart cast는 when 표현식과 while 루프안에서도 동작한다.
when (x) {
is Int -> print(x + 1)
is String -> print(x.length + 1)
is IntArray -> print(x.sum())
}
smart cast는 컴파일러가 Type check 와 사용 사이에 변수가 변경되지 않음을 보장할 수 있는 경우에만 동작한다. 보다 구체적으로, 아래의 조건에서 사용할 수 있다.
- val 지역변수 : 지역 위임 속성을 제거하고 항상 동작함
- val 속성(properties) : 접근 제한자가 private 또는 internal 이거나 해당 속성이 선언된 동일한 모듈에서 검사가 수행되는 경우 동작함. ( * open 속성이나 커스텀 getter 가 존재하는 속성에 대해서는 동작하지 않는다.)
- var 지역변수 : 지역 위임 속성이 아니고, 수정하는 람다에 사용되지 않는 type check와 참조 사이에 변수가 수정되지 않는 경우 동작
- var 속성 : 절대 동작하지 않음. 언제든지 변경될 수 있기 때문이다.
"Unsafe" cast operator
일반적으로, cast 연산자는 캐스트가 가능하지 않는 경우, 예외를 throw 한다. 그래서 안전하지 않다고 한다. Kotlin의 안전하지 않은 캐스트는 infix 연산자 as에 의해 수행된다.
val x: String = y as String
이 유형은 nullable 이 아니기 때문에 null을 String으로 캐스팅할 수 없다. 만약에 위 코드에서 y가 null이라면, 예외가 발생한다. 올바른 코드는 다음과 같다.
val x: String? = y as String?
"Safe" (nullable) cast operator
예외를 방지하려면 실패 시 null을 반환하는 안전 캐스트 연산자 as?를 사용해야 한다. as? 연산자를 사용하는 경우 캐스팅이 불가능하다면 null을 반환한다.
val x: String? = y as? String
Type erasure and generic type checks
Kotlin은 컴파일 시간에 제네릭과 관련된 작업에 대한 Type 안전성을 보장하지만 런타임에 제네릭 형식의 인스턴스는 실제 형식 인수에 대한 정보를 보유하지 않습니다. 일반적으로 인스턴스가 런타임에 특정 형식 인수를 가진 제네릭 형식에 속하는지 확인할 수 있는 방법은 없다.
이 때문에, 컴파일러는 ints is List<Int> 또는 list is T 같은 유형 삭제로 인해 런타임에 수행할 수 없는 is-checks를 금지한다. 그러나, start-projected type을 이용하면, 인스턴스 타입 체크가 가능하다.
if (something is List<*>) {
something.forEach { println(it) } // The items are typed as `Any?`
}
만약, 정적으로 검사된 인스턴스의 type argument(타입 인수)가 있는 경우(컴파일 시간에)Type의 제네릭이 아닌 부분을 포함하는 is-check 또는 캐스트를 만들 수 있다. 이 경우, 꺽쇠 괄호(<>)가 생략된다.
fun handleStrings(list: List<String>) {
if (list is ArrayList) {
// `list` is smart-cast to `ArrayList<String>`
}
}
동일한 구문이지만, type 인수가 생략된 경우 type 인수가 고려하지 않는 캐스트에 list as ArrayList를 사용할 수 있다.
구체적으로 명시된 type 매개변수가 있는 inline 함수에는 각 호출부에서 인라인된 실제 type 인수가 있다. 이렇게 하면 arg is T가 형식 매개변수를 확인할 수는 있지만, arg가 제네릭 형식 자체의 인스턴스인 경우 해당 형식 인수는 여전히 지워진다.
근데 아직 잘 이해가 안가서 제네릭 파트 문서 다시 읽고 더 공부해봐야겠다!
Unchecked casts
위에서 봤듯이, type eraser은 런타임시에 제네릭 형식 인스턴스의 실제 형식 인수를 확인하는 것을 불가능하게 만든다. 또한, 코드의 제너릭 형식은 컴파일러가 형식 안전성을 보장할만큼 충분히 서로 밀접하게 연관되어 있지 않을 수 있다.
대신, 코틀린은! 타입 안전성을 암시하는 높은 수준의 프로그램 로직을 가지고 있다!
fun readDictionary(file: File): Map<String, *> = file.inputStream().use {
TODO("Read a mapping of strings to arbitrary elements.")
}
// We saved a map with `Int`s into this file
val intsFile = File("ints.dictionary")
// Warning: Unchecked cast: `Map<String, *>` to `Map<String, Int>`
val intsDictionary: Map<String, Int> = readDictionary(intsFile) as Map<String, Int>
위 코드의 마지막 줄에서, 캐스트 경고가 나타난다. 컴파일러는 런타임에 이를 완전히 확인할 수 없으며, Map의 값이 Int임을 보장하지 않는다.
체크되지 않은 캐스트를 피하기 위해 프로그램 구조를 다시 디자인할 수 있다. 위의 예시에서 다른 유형에 대한 유형 안전 구현과 함께 DictionaryReader<T> 및 DictionaryWriter<T> 인터페이스를 사용할 수 있다. 확인되지 않은 캐스트를 호출부에서 구현 세부정보로 이동하기 위해 합리적인 추상화를 도입할 수 있다. 제네릭스의 변성을 적절히 사용하는 것도 도움이 된다.
제네릭스 함수의 경우, 구체화된 타입의 매개변수 (reified type parameters)를 사용하면 인수의 타입에 지워지는 자체 타입 argument가 있지 않는 한 arg as T 검사와 같은 캐스트를 만든다.
체크되지 않은 캐스트 경고는, @Suppress("UNCHECKED_CAST") 애노테이션을 해당 코드 또는 선언부에 추가해서 억제할 수 있다.
inline fun <reified T> List<*>.asListOfType(): List<T>? =
if (all { it is T })
@Suppress("UNCHECKED_CAST")
this as List<T> else
null
❕JVM 플랫폼에서!
array types (Array<Foo>)은 요소의 지워진 유형에 대한 정보를 유지하고, 배열 유형에 대한 유형 캐스트는 부분적으로 확인된다. 요소 유형의 null 허용 여부 및 실제 유형 인수는 여전히 지워진다.
예) foo as Array<List<String>?>의 경우, foo가 null 허용 여부에 관계없이 List<*>를 포함하는 배열인 경우 성공