안전한 캐스트 "as?"
기본적으로 "as" 연산자는 대상 값을 as로 지정한 타입으로 캐스트 하며, 해당 타입으로 바꿀 수 없으면 ClassCastException이 발생하게 됩니다.
이럴 경우 is 연산자를 통해 대상 값이 해당 타입으로 변환 가능한 타입인지 체크해야 하지만 코틀린에서는 훨씬 더 간결한 기능으로 "as?" 연산자를 제공합니다.
"as?" 연산자는 어떤 값을 지정한 타입으로 캐스트를 하고, 만약 대상 타입으로 캐스트 할 수 없으면 null을 반환하게 됩니다.
/* 안전한 캐스트를 사용해 equals 구현하는 예제 */
class User(val id: String) {
override fun equals(o: Any?): Boolean {
val otherUser = o as? User ?: return false //타입이 맞지 않으면 false 반환
return otherUser.id == id //안전한 캐스트를 하고 나면 otherUser가 User로 스마트캐스트 된다
}
}
>>> val user1 = User("Happy")
>>> val user2 = User("BTS")
>>> println(user1 == user2)
true
>>> println(user1 == 123) //User객체가 아닌 엉뚱한 타입과 비교
false
null이 아닌 값 "!!"
null이 아닌 값(not-null assertion)은 코틀린에서 느낌표를 이중(!!)으로 사용하면 어떤 값이든 null이 될 수 없는 타입으로 바꿀 수 있습니다.
그렇기 때문에 실제 null 값에 대해 !!를 적용하면 NullPointerException이 발생합니다.
/* null 아님 !! 예제 */
fun ignoreNull(s: String?) {
val stringNotNull: String = s!! //예외는 이 시점에서 발생됩니다.
println(stringNotNull.length) //stringNotNull은 null이 아닌 값으로 인식됩니다.
}
>> ignoreNull(null)
Exception in thread "main" kotlin.KotlinNullPointerException...
근본적으로 !!는 컴파일러에게 개발자가 "이 값이 null이 아님을 확신한다! 만약 내 생각이 잘못됐다면 예외가 발생해도 감수하겠다" 라는 의미로 볼 수 있습니다.
!! 기호의 모습 자체를 마치 컴파일러에게 소리를 지르는 듯한 느낌으로 코틀린 설계자들이 일부러 의도한 것이라고 합니다.
컴파일러가 검증할 수 없는 null 아님(!!) 을 사용하기보다는 더 나은 방법을 찾아보라는 의도로 !! 라는 기호를 택했다고 합니다. 😮
Tip !
null 아님(!!) 을 사용하다가 발생하는 exception의 stackTrace에는 어떤 파일의 몇 번째 줄인지에 대한 정보는 들어있지만 어떤 식에서 예외가 발생했는지에 대한 정보는 들어있지 않습니다.
그렇기 때문에 어떤 값이 null이었는지 확실히 파악하기 위해 !! 문을 한 줄에 연달아 나열해서 함께 쓰는 일은 반드시 피해주는 것이 좋습니다.
예시) user!!.address!!.country
이런 식으로 코드를 작성하면 어느 객체에서 NPE가 발생했는지 파악하기가 어렵습니다...! 😨
"let" 함수
let 함수를 사용하면 null이 될 수 있는 식을 쉽게 다룰 수 있습니다.
let 함수를 사용하는 가장 흔한 사례는 안전한 호출 연산자 (?.) 와 함께 사용하여 원하는 식의 결과가 null인지 검사한 다음 그 값을 전달받아 처리하는 경우입니다.
/* 안전한 호출 연산자 ?.와 let 함수의 사용 예제 */
carInfo?.let { //carInfo가 null이 아닌 경우에만 아래 로직 실행
mTextCarName.setText(it.name)
mTextCarPrice.setText(it.price)
}
안전한 호출 연산자 "?."를 사용하여 let을 호출하되 null이 아닌 값을 let 함수의 수신 객체로 람다에게 전달합니다.
나중 초기화 "lateinit"
생성자에서 해당 변수를 초기화하는 경우와 같이 반드시 null이 될 일이 없는 변수를 선언하고 초기화하는 경우에는 "lateinit" 을 변수 앞에 붙이면 나중에 초기화할 수 있습니다.
"lateinit"이 붙는 변수는 반드시 var 로 선언되어야 합니다.
/* lateinit을 사용하지 않은 경우 */
class MainActivity : AppCompatActivity() {
private var myService: MyService? = null //null이 가능한 변수로 선언
override fun onCreate(savedInstanceState: Bundle?) {
myService = MyService() //이렇게 생성된 myService 변수는 null 가능이기 때문에
//다른곳에서 사용할 때마다 null 체크를 해줘야 한다.
}
}
/* lateinit을 사용한 경우 */
class MainActivity : AppCompatActivity() {
private lateinit var myService: MyService //null이 될 수 없는 변수로 선언
override fun onCreate(savedInstanceState: Bundle?) {
myService = MyService() //null 체크 필요 없이 사용이 가능하다.
}
}
null이 될 수 있는 타입에 대한 확장 함수
코틀린의 확장 함수 기능을 활용하면, null이 될 수 있는 변수에 대해 안전한 호출 연산자를 쓰지 않아도 확장 함수에서 null 체크를 해주는 기능을 구현할 수가 있습니다.
예를 들면, 코틀린 String의 확장 함수인 isNullOrEmpty, isNullOrBlank 메소드가 있습니다.
/* null이 될 수 있는 수신 객체에 대해 확장 함수 호출하기 */
fun sendEventLog(event: String?) {
if(event.isNullOrBlank()) { //안전한 호출 연산자 ?.를 하지 않아도 된다.
...
}
}
다음은 정의된 isNullOrBlank 확장 함수의 구현체입니다.
fun String?.isNullOrBlank(): Boolean = //null이 될 수 있는 String으로 확장
this == null || this.isBlank()
null이 될 수 있는 타입에 대한 확장을 정의하였기 때문에 위의 예제에서 event 문자열이 null이 될 수도 있음에도 안전한 호출로 null 체크를 하지 않고 확장 함수를 호출할 수 있었던 이유입니다.
Tip !
확장 함수를 만드는 경우, 처음에는 null이 될 수 없는 타입에 대한 확장 함수로 먼저 정의를 해봅니다.
그러곤 나중에 이 확장 함수를 사용하게 되는 경우 null이 될 수 있는 타입에 대해 이 함수를 호출했다는 사실을 알게 되면 그때 확장 함수 안에서 null 체크를 제대로 처리해주게 되면 null에도 안전한 확장 함수가 작성될 수 있습니다. ^^
'코틀린' 카테고리의 다른 글
코틀린(Kotlin) - 원시 타입(primitive type) (0) | 2021.04.14 |
---|---|
코틀린(Kotlin) - 타입 파라미터의 null 가능성 (0) | 2021.03.30 |
코틀린(Kotlin) - 타입 시스템 null 가능성 safe call(?.) elvis(?:) (0) | 2021.03.21 |
코틀린(Kotlin) - lambda with receiver(수신 객체 지정 람다) : with, apply (0) | 2021.03.16 |
안드로이드 코루틴 기본 개념과 활용까지의 모든 것! (3) | 2021.03.08 |