타입 캐스팅 연산자(as)는 대상 값을 지정한 타입으로 변환할 수 없을 경우 ClassCaseException를 발생시킨다.
코틀린은 이 문제에 safe cast라는 해법을 제공한다. safe cast 연산자 as?는 as와 같이 지정한 타입으로 변환을 시도하지만 불가능할 경우 null을 반환한다.
val number = 0
val str: String? = number as? String
>>> print(str)
null
위 예시에서 Int 타입 number를 String으로 변환하여 str 변수에 저장을 시도한다. 하지만 그것은 불가능하므로 as?에 의해 null이 반환되어 저장된다.
safe cast의 일반적인 사용 패턴은 엘비스 연산자와 함께 사용하는 것이다. 예시는 아래와 같다.
class Person(val firstName: String, val lastName: String) {
override fun equals(o: Any?): Boolean {
val otherPerson = o as? Person ?: return false
return otherPerson.firstName == firstName &&
otherPerson.lastName == lastName
}
}
>>> val p1 = Person("길동", "홍")
>>> val p2 = Person("길동", "홍")
>>> println(p1 == p2)
true
>>> println(p1.equals(42))
false
equals 구현을 보면 as?로 같은 타입으로 변환을 시도하고 아닐 경우 elvis 연산자를 통해 false를 반환하고 있다.
이처럼 safe cast를 사용하면 안전하게 타입 캐스팅을 하면서 대체 동작까지 한 줄에 정의할 수 있다.
safe call, elvis 연산자, safe cast과 달리 non-null assertion 연산자인 !!은 nullable 타입 값을 검사 없이 non-null 타입으로 강제 변경한다. 하지만 그 값이 null일 경우 NPE가 발생한다.
fun ignoreNulls(s: String?) {
val sNotNull: String = s!!
println(sNotNull.length)
}
>>> ignoreNulls(null)
// KotlinNullPointerException 발생!
여기서 주목할 점은 sNotNull에 접근하는 지점이 아니라 !!로 변환하는 지점에서 예외가 발생한다.
편한 만큼 리스크가 크기 때문에 사용이 권장되진 않는다. 생김새가 !!로 소리치는 것처럼 만든 것도 더 나은 방법을 찾아보라는 이유에서 그랬다고 한다.
person.company!!.address!!.country
또한, 이렇게 연쇄해서 사용할 경우 정확히 어느 식에서 예외가 발생했는지 알려주지 않으므로 한 줄에 사용하는 것은 피하는 게 좋다.
let 함수를 사용하면 null이 될 수 있는 식을 더 쉽게 다룰 수 있다.
가장 많이 사용되는 패턴은 safe call과 함께 사용하여 nullable 값을 non-null 값으로 다루는 것이다.
val email: String? = ...
email?.let { e: String -> sendEmail(e) }
let 함수는 자신의 수신 객체를 인자로 전달 받은 람다에게 넘긴다. 여기서는 email이 수신 객체다.
safe call(?.)을 사용하므로 email 값이 null이 아니면 let 함수가 호출되면서 람다 블록이 실행되고, null일 경우 아무 일도 일어나지 않고 결과 값은 null이 된다.
또한, 위 예시는 아래처럼 축약이 가능하다. let은 람다 안에서 파라미터에 이름을 짓지 않을 경우 it으로 수신 객체에 접근할 수 있다.
email?.let { sendEmail(it) }
이 식은 if문으로 null 검사를 하는 것과 같은 효과를 낸다. 따라서, if문보다 let을 사용하는 것이 더 간결할 경우 let 함수를 선택하자.
반면, 여러 값에 null 검사를 해야할 경우 let 함수를 중첩해서 사용할 수 있다. 그러면 중괄호가 중첩되어 코드가 복잡해지므로 그럴 땐 일반적인 if문으로 모든 값을 한꺼번에 검사하는 것이 낫다.
person?.let { p ->
//...
p.company?.let { c ->
//...
c.address?.let {
//...
}
}
}
if (person != null && person.company != null) { /*...*/ }