[HeadFirst] Kotlin nullable type

timothy jeong·2021년 10월 29일
0

코틀린

목록 보기
10/20

kotlin 은 non-nullable type 과 nullable type 이 나뉜다. Java 에서는 두 타입을 나누지 않았지만, kotlin 에서는 이 둘을 나눠놓음으로써 NullPointerException 을 획기적으로 줄였다.

nullables

nullabe type 은 non-nullable type 을 쓸 수 있는 곳이라면 어디든 정의 할 수 있다. 심지어 Java 의 Primitive type 에 해당하는 Int, Double, Boolean 등에도 붙일 수 있다.

fun main() {
    var o: Hippo? = Hippo()
    o = null    

nullables 는 변수 선언 뿐만 아니라 funtion 의 argument 로도 사용될 수 있다.

fun main() {
    var a: Int? = null
    var b: Boolean? = null
    val c: Int = 10
    myFun(c)
}

fun myFun(a :Int?) : Int? {
    return 1
}

특이한 사항이라면 Int? type 을 인자로 받지만 일반 Int 형을 넘겨주어도 아무런 문제가 되지 않는다는 것이다. 그리고 함수의 return type 으로도 사용할 수 있다.

Array 도 역시 nullable 이 가능하다.

fun main() {
    var arr: Array<String?> = arrayOf(null, "hi", "bye")
}

access a nullable type

nullable type 을 도입해서 NullPointerException 를 획기적으로 줄였다고 했는데, 이는 사실 nullable type을 도입해서 그렇다기 보다는 이를 도입하는 동시에 해당 type 에 접근하는 더 안전한 방법을 제공했기 때문일 것이다.

fun main() {
    var wolf: Wolf? = Wolf()
    
    if (wolf != null) {
        wolf = null
        wolf.eat() // NullPointerException
    }

평범하게 if 로 null 검사를 하게 되면, var 로 선언된 변수일 경우 그 중간에 null 로 값이 바뀔 수도 있기 때문에 더 안전한 방법이 제공되는게 맞다.

safe call ?.

.? 연산자는 nullable type 함수나 프로퍼티에 안전하게 접근할 수 있게 해준다.

fun main() {
    var my: MyClass? = MyClass(10)
    my?.say()
    my = null
    my?.say()
    
}

class MyClass(val num:Int) {
    fun say() : Int{
        println("my number is $num")
        return num
    }
}

.? 연산자는 '주어진 변수가 참조하는 값이 null 이 아니라면 say() 를 호출하라' 이런 식으로 시행된다. 그래서 error 도, 어떤 메시지도 출력되지 않는다.

safe call 활용

safe call 은 chain 을 만들 수도 있고 값을 할당하는데 활용할 수도 있다.
위의 MyClass 코드 예제에서 이어서 아래와 같이 safe call chain 을 만들어서 사용할 수 있다. 이때는 my? 부터 검사되고, 그 다음에 m? 이 검사된다.

그리고 모든 검사에서 null 이 아니라면 myNum 에는 1이라는 값이 할당이 된다. 만약 둘 중에 하나라도 null 이라면 myNum 역시 null 이 될 것이다.

fun main() {
    var my : MySecondClass? = MySecondClass()
    var myNum:Int = my?.m?.say()
}

class MySecondClass() {
    var m: MyClass? = MyClass(1)
}

none type but null value
위의 myNum 의 변수처럼 변수 타입이 할당되어 있지 않은데, my?.m?.say() 의 결과 null 이 할당된다면 변수 타입은 어떻게 될까? 이때는 컴파일러가 어떠한 정보도 받지 못했기 때문에 myNum 은 null 값만 담을 수 있는 변수 타입이 된다. 대부분의 경우 이런 변수를 사용하지 않을 것이기 때문에 이런 일이 없도록 조심해야한다.

let 연산

좀더 안전하게 접근하는 방법은 알겠다. 하지만 특정 값이 null 이 아닐때 특정 코드를 시행하고 싶으면 그 코드를 다 메서드로 만들 수는 없지 않을까? if else 문을 이용해서 검증하는건 이미 문제를 알았으니, 새로운 방법이 필요하다.

이때 사용할 수 있는게 let 연산이다.

fun main() {
    var my : MySecondClass? = MySecondClass()
    my?.m?.say()

    my?.let { 
        println("var my is not null")
        it.m?.say()
    }
}

?.let 연산은 마치 is 연산처럼 { } 내부 코드에서는 주어진 변수가 null 이 아니라는게 보장됐다는 것이다. 이때 it 키워드를 이용해서 검사 대상이었던 변수의 non-nullable type 버전의 변수를 이용할 수 있다. 그렇기 때문에 it은 ?. 이 아닌 . 만으로도 내부 프로퍼티에 접근이 가능하다.

Elivs 연산

let 은 사실 if else 가 아니라 그냥 if 아닌가? if else 에 해당하는 연산자가 필요하다. ?: 는 Elivs 연산자라고 불리며 우리가 원하는 기능을 수행한다.

fun main() {
    var my : MySecondClass? = MySecondClass()
    my?.m?.say()
    val myNum = my?.m?.say() ?: -1
    println(myNum)
}

class MyClass(val num:Int) {
    fun say() : Int{
        println("my number is $num")
        return num
    }
}

class MySecondClass() {
    var m: MyClass? = null
}

MySecondClass 의 m 프로퍼티를 null 로 바꿨기 때문에 my?.m?.say() 부분에서 null 이 나올것이다. 이때 Elvis 연산자가 else 역할을 하기 때문에 myNum 은 -1 값을 할당 받고, 출력값도 -1이 나온다.

not-null assertion operator !!

이전에 나온 ?. , ?.let, ?. --- ?: --- 연산자들이 더 안전하게 nullalbe type 을 다루기 위해 나온것과는 달리 !! 연산자는 직접 NullPointerException 을 날리기 위해 사용된다.

아래와 같은 코드에서 safe call chain 중에서 하나라도 null 이라면 NullPointerException 이 발생한다. 디버그용으로 유용하다!

fun main() {
    var my : MySecondClass? = MySecondClass()
    val myNum = my?.m!!.say()
}
profile
개발자

0개의 댓글