is와! is 연산자
is와! is를 사용하는 것은 런타임에서 객체가 주어진 타입을 준수하는지 여부를 식별한다.
if (obj is String) {
print(obj.length)
}
if (obj !is String) { // same as !(obj is String)
print("Not a String")
} else {
print(obj.length)
}
Smart casts
is를 사용하는 대부분의 경우 명시적 캐스트 연산자를 사용할 필요 없다. 코틀린 컴파일러가 불변 값에 대한 is 검사 및 명시적 캐스트를 추적하고 필요한 경우 자동으로 (안전한) 캐스트를 삽입한다.
class Ma {
fun test(x: Any){
//1번
println(x.length) //Unresolved reference: length
if(x is String){
//2번
println({x.length})
}
}
}
1번 에서 x에 length 프로퍼티를 참조하려고 하면 알 수 없는 표현 에러가 뜨면서 참조가 불가능하다. Any 클래스엔 length 프로퍼티가 없기 때문이다. 하지만 2번 위치에선 정상적으로 참조 가능하다.
이것으로 알 수 있는 것은 is 연산자를 거친 이후 x의 타입이 Any 타입이 아닌 String 타입으로 변경 됐다는 것이다.
class Ma {
fun test(x: Any){
if(x !is String) return
println(x.length)
}
}
마찬가지로 부정 검사가 반환으로 이어지는 경우 캐스트가 안전하다는 것을 알 만큼 컴파일러는 똑똑하다.
비슷하게 참, 거짓을 반환하는 비교 연산자(||, &&)의 경우 결과가 확정된 시점에 자동으로 캐스트 한다.
//Unresolved reference: length
if(x.length == 0 || x !is String) return
//Unresolved reference: length
if(x.length > 0 || x is String) return
// ||의 오른편에 있는 x는 String로 자동 캐스팅 된다
if( x !is String || x.length == 0) return
// &&의 오른편에 있는 x는 String로 자동 캐스팅 된다
if(x is String && x.length > 0) print(x.length)
when 식과 while루프에서도 작동한다.
fun test(x: Any){
when(x){
is Int -> print(x + 1)
is String -> print(x.length + 1)
is IntArray -> print(x.sum())
}
}
Smart Cast는 컴파일러가 변수가 검사하거나 사용하는 도중 변하지 않는다고 보장할 때 작동한다.
보다 자세하게 Smart Cast는 아래 조건에서 사용할 수 있다.
- val 로컬 변수(local variables) - 로컬 위임 속성을 제외하고 항상
- val 속성(properties) - 속성이 전용 또는 내부인 경우 또는 속성이 선언된 동일한 모듈에서 검사가 수행된 경우. 열린 속성이나 사용자 정의 getter가 있는 경우 사용할 수 없음
- var 로컬 변수(local variables) - 변수가 검사와 사용 도중 수정되지 않고, 변수를 수정하는 람다에 사용되지 않으며, 로컬 위임 속성이 아닌 경우.
- var 속성(properties) - 절대 불가능, 언제든 수정될 가능성이 있기 때문
“안전하지 않은” 캐스트 연산자
일반적으로 캐스트 연산자는 불가능 할 경우 예외를 날린다. 이를 안전하지 않다(unsafe)라고 한다. 안전하지 않은 연산자는 중위 연산자 ‘as’를 통해 수행될 수 있다.
val x: String = y as String
이 유형은 null을 허용하지 않으므로 캐스팅 할 수 없다. 다시 말해 null인 경우 위 코드는 예외를 던진다는 말이다. 만약 null 값에 대해 코드를 올바르게 만들고 싶다면 as 오른쪽의 타입을 nullable 하게 수정하면 된다.
val x: String? = y as String?
“안전한” 캐스트 연산자
보다 안전하게 예외를 방지하려면 실패 시 반환되는 안전한(safe) 캐스트 연산자를 사용하면 된다.
val x: String? = y as? String
방금 전 nullable하게 수정한 코드와 차이점은 as? 오른쪽의 타입이 null을 허용하지 않아도 null을 반환한다는 점이다.
그렇다면 여기서 생기는 의문점
as 의 오른쪽 타입을 nullable 하게 만들어주는 방법과 as? 을 쓰는 것에 무슨 차이가 있을까?
둘 다 null값에 대해 올바르게 반환 한다면 아무거나 써도 되는 것일까?
fun main() {
//String 일 경우
safeOperator("testvalue")
unSafeOperator("testvalue")
//null 일 경우
safeOperator(null)
unSafeOperator(null)
//Int 일 경우
safeOperator(1234)
unSafeOperator(1234)
}
fun <T> safeOperator(x: T){
val testValue = x as? String
println(testValue)
}
fun <T> unSafeOperator(x: T){
val testValue = x as String?
println(testValue)
}
정답은 “안된다” 이다. 결과를 보면 알 수 있다.
testvalue
testvalue
null
null
null
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
at AaaaaKt.unSafeOperator(aaaaa.kt:19)
at AaaaaKt.main(aaaaa.kt:10)
at AaaaaKt.main(aaaaa.kt)
String값을 넣었을 때는 당연하게도 정상적으로 출력 됐다.
우리가 주의 깊게 살펴봐야 하는 것을 null값을 넣었을 때와 정수 값을 넣었을 때다. null 값을 넣었을 때는 정상적으로 null이 출력 됐다. 하지만 정수 값을 넣었을 때 safeOperator에선 null이 출력됐지만 unSafeOperator에선 ClassCastException이 발생했다.
분명 null 허용이라고 했는데 안 되는 이유가 뭘까?
수정된 안전하지 않은 연산자는 null 을 허용만 하기 때문이다.
안전한 연산자는 타입을 검사하여 변경할 수 없는 경우 null을 반환하는 것이고, 안전하지 않은 연산자는 String 타입에 null 도 들어갈 수 있게 해 놓은 것이라 볼 수 있다.
따라서 제네릭으로 선언한 매개변수에 대해선 안전한 연산자(as?)를 통해 타입을 검사하는게 맞다.