ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Kotlin] Null Safety
    Kotlin/Null Safety 2021. 8. 9. 14:19

    자바도 부족한데 코틀린 코드를 쓰려니 컴파일 에러가 자꾸나서....null부분 reference를 다시 한번 보러 들어갔다.

    코틀린 Null Safety 부분을 보고 모르는 것만 정리했다.

     

    코틀린에서는 null과 관련하여 non-null 변수 nullable변수 두가지 종류가 있다.

    변수 선언시 타입뒤에 ?를 통하여 nullable변수임을 명시한다.

    var str: String //non-null변수
    var str: String? //nullable변수

    non-null변수에는 명시적으로 null로 초기화가 불가능하다(컴파일 에러)

    nullable변수는 null이 들어갈 수 있는 변수로 멤버( 프로퍼티 접근자(getter,setter), 메서드 ) 를 사용하기 전에는 null체크를 해야한다. 그렇지 않으면 컴파일 에러가 발생한다.

     

    non-null변수에는 non-null변수만 대입시킬 수 있다.

    nullable변수에는 문법적으로는 nullable변수, non-null변수 모두 대입시킬 수 있다.

    (굳이 null이 들어갈 수 없는 non-null변수를 nullable변수에 대입시키는 일은 없겠지만, 문법적으로는 가능하다)


    println(null)이 가능하다


    조건문 안에서 null체크를 하면 컴파일러가 변수의 상태가 null인지 아닌지를 tracking한다.

    fun main() {
    
        var b: String? = "kotlin"
        if (b != null) { //null체크
            println("${b.length}") //null이 아니기 때문에 length사용가능
        } else {
            println("null입니다")
        }
    }

    nullable변수 b의 null체크가 진행되었기 때문에 괄호구간에서 b.length를 사용할 수 있다.

     

    fun main() {
        
        var a2: String? = "kotlin"
        
        if(a2!=null){
            println(a2.length)
        }else{
            println("a2는 null")
        }
        println(a2.length) //error!!! - null체크가 된 구간에서만 사용가능
    }

    null체크가 된 구간에서만 a2의 멤버를 사용할 수 있다.

     

    좀 더 복잡한 경우

    val b: String? = "Kotlin" //val 변수
    if (b != null && b.length > 0) {
        print("String of length ${b.length}")
    } else {
        print("Empty string")
    }

    코틀린 컴파일러는 똑똑하다.

    if문의 조건식을 보면 두개의 expression(전자 && 후자) 이 logical and인 &&로 결합되어있다.

    &&는 short-circuit evaluation이다.

    전자가 false일 경우, 후자가 true이든 false이든 전체결과는 항상 false임을 예측 가능하기때문에

    후자는 평가되지 않는다.

    전자가 true일 경우에만, 후자의 평가가 이루어진다.

     

    b가 null이면 전자가 false로 후자의 b.length는 평가되지 않고

    b가 null이 아니면 후자의 length는 사용이 가능하다.

    결과적으로 컴파일 에러가 발생하지 않는다.

     

    전자와 후자의 순서를 바꿔보면 컴파일 에러가 발생하고 IDE는 null체크를 하라는 제안을 해준다.

    (length를 먼저 사용하게 되므로)

     

    다음의 logical or 도 컴파일 에러가 발생한다.

    //컴파일 에러
    if (b != null || b.length > 0) {
    } else {
    }

    전자가 true라면 후자에 관계없이 전체 결과는 true로 결정 내릴 수 있다. 이때는 문제가 없지만

    전자가 false라면 (b가 null이면) 후자를 평가하는 과정에서 b.length가 안되는 상황이 존재하기 때문에

    컴파일 에러가 발생한다.

     

    다음과 같은 코드는 문제가 없다.

    fun main() {
        var b: String? = "kotlin"
    
        if (  (false || b != null)  && b.length > 0) {
        } else {
        }
    }

    b!=null은 항상 평가가 되기 때문에

     

    다음과 같이 b!=null이 평가가 안될 수 있는 경우에는 컴파일 에러가 발생한다. 

    fun main() {
        
        fun test(): Boolean {
            var a=10
            return if (a < 10) true else false
        }
        
        var b: String? = "kotlin"
    
        if ( (test() || b != null) && b.length > 0) { //b.length 에러
        } else {
        }
    }

    바로 위의 내용을 찾아보다가 어떤 사람이  var b =null로 해놓고 똑같은 질문을 올렸다.

    //컴파일 에러 코드
    val b = null 
    if (b != null && b.length > 0) {
        print("String of length ${b.length}")
    } else {
        print("Empty string")
    }

    non-null변수에는 명시적으로 null초기화가 불가능해서 b가 nullable타입으로 되는 것은 알겠는데 

    inferred type이 어떻게 되는가 궁금했다. IDE에서 띄워주는 것을 보니 Nothing 타입이였다.

    추후에 대입되는 타입에 맞춰서 타입이 정해지는줄 알았는데 아니였다.

     

    var b=null을 디컴파일 해봤더니 자바로는 Void b=null; 가 되었다.

    public final
    class Void {
    
        /**
         * The {@code Class} object representing the pseudo-type corresponding to
         * the keyword {@code void}.
         */
        @SuppressWarnings("unchecked")
        public static final Class<Void> TYPE = (Class<Void>) Class.getPrimitiveClass("void");
    
        /*
         * The Void class cannot be instantiated.
         */
        private Void() {}
    }

     

    Nothing에 대해서 공부

     

    Nothting과 Nothing?

    https://readystory.tistory.com/143

     

    [Kotlin] 헷갈리는 "Nothing" 확실하게 이해하기(feat. Any, Unit)

    기존에 자바에 대한 지식이 있는 상태에서 코틀린으로 넘어오신 분들은 코틀린에서 제공하는 대다수의 타입과 클래스들에 대해 거부감이 없으실테지만, 그럼에도 Any와 Unit까지는 어찌어찌 이

    readystory.tistory.com


    Safe calls - 안전한 호출 

     

    안전한 호출에서 알 수 있듯이 변수를 통해 메서드, 프로퍼티 접근자 등등을 안전하게 호출할 수 있다는 것이다.

    nullable한 변수는 null이 들어가 있을 수도 있기 때문에 null체크를 해야하는데 null체크 대신 safe call을 사용하면 

    메서드나 프로퍼티 접근자등등을 바로 호출할 수 있다.

     

    safe call을 사용하였을 때 nullable한 변수에 null이 들어있을 경우 메서드등등을 호출하면

    메서드를 호출이 진행되지 않고 null을 리턴한다.

     

    println(null)이 가능하다는 것을 염두하자.

    fun main() {
    
        //a1는 nullable String변수
        var a1: String? = null
        a1.length //error!!!
        //a1가 null일수도 있기 때문에 a.length를 사용할 수 없다.
    
        var a2: String? = null
        a2?.length 
        //safe call
        //a2?.length의 리턴 타입은 Int? 가 된다
        //a2가 non-null인 경우 Int리턴 , a2가 null인 경우 null리턴
        println(a2?.length) //null
    
        var a3: String? = "kotlin"
        var b3: Int
        b3 = a3?.length //error
        //a3?.length는 Int?를 리턴한다.
        //Int타입 b3에 Int?타입을 대입하여 컴파일 에러
    
        var a4: String? = null
        var b4: Int?
        b4 = a4?.length //b4를 Int?형으로 선언하여 컴파일 에러는 막았다.
        println(b4) //null
    
        var a5: String = "kotlin"
        var b5: Int
        b5 = a5?.length 
        //non-null변수에 safe call을 사용하는 것은 하나마나인 코드이다.
        //또한 safe call을 사용했다고 해서 a5.length의 리턴타입이 Int?로 바뀌지 않음을 확인하였다.
        //결과적으로 safe call은 그냥 null이면 null로 리턴하는 역할을 한다
        
    }

     

    safe call은 chain일 때 유용하게 쓰일 수 있다.

    fun main() {
    
        var str: String?="kotlin"
        
        println(str?.length.plus(1)) 
        //error- str?.length가 null일수 있기 때문에 plus를 사용할 수 없다
        
        println(str?.length?.plus(1)) //7 
        //chain으로 length에 safe call추가 
        //str?.length가 null이 아니면 plus를 메서드를 리턴시키거나
        //str?.length가 null이면 plus메서드 호출 대신 null을 리턴시킨다.
        
    }

     

    safe call은 대입식의 좌변에 위치할 수도 있는데 safe call chain중 하나라도 null이면 우변 대입은 실행되지 않는다.

    class Test(){
        var name:String=""
    }
    
    fun main() {
        var test: Test = Test()
        test.name = "홍길동"
    
        //safe call test2
    
        var test2: Test? = Test()
        test2.name = " 홍길동" //컴파일 에러- test2 null일 수 있음
    
        var test3: Test? = Test()
        test3?.name = "홍길동" 
        //test3가 null이 아니므로 name프로퍼티에 값이 할당됨
    
        var test4: Test? = null
        test4?.name = "홍길동" 
        //test4가 null이므로 name프로퍼티에 "홍길동"을 대입하는 식이 실행되지 않는다.
        
    }

     

    not null과 nullable이 섞여있을 때 safe call과 let 키워드를 사용하라고 되있다. let에 대해서는 코틀린의 추가적인 메서드를 공부한 뒤 알아봐야겠다.

    val listWithNulls: List<String?> = listOf("Kotlin", null)
    for (item in listWithNulls) {
        item?.let { println(it) } // prints Kotlin and ignores null
    }

    https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/let.html

     

    let - Kotlin Programming Language

     

    kotlinlang.org

    https://kotlinlang.org/docs/scope-functions.html#let

     

    Scope functions | Kotlin

     

    kotlinlang.org


    Elvis operator - ?:

     

    표현식 ?: 기본값설정

    표현식이 null이 아니면 표현식의 값을 그대로 사용하고, 표현식이 null이면 기본값이 사용된다.

     

    safe call은 변수가 null일때 변수.멤버를 호출하면 null을 뱉어낸다.

    그렇기 때문에 멤버 호출결과를 다른 변수에 대입할 때 항상 nullable한 변수에 대입해야 한다.

    또 그 담은 변수도 nullable변수이기 때문에 사용하려면 null체크를 하여 사용하여야 하는 불편함이 있다.

    safe call과 elvis operator를 같이 사용하면 더 편해진다.

    fun main() {
    
        var str: String? = "kotlin"
    
        var leng: Int = str?.length ?: -1
        //str이 null이면 safe call은 null을 리턴하고 elvis operator에 의해 -1값이 된다.
        println(leng) //6
    
        str = null
        leng = str?.length ?: -1
        println(leng) //-1
    
    }

     

    leng 변수를 non-null변수로 하여 값을 받을 수 있다.  -1이라는 값을 통해 leng의 유효성을 체크한다.

     

     

    elvis operator의 우변에는 throw나 return을 사용할 수 있다.

    fun foo(node: Node): String? {
        val parent = node.getParent() ?: return null
        val name = node.getName() ?: throw IllegalArgumentException("name expected")
        // ...
    }

    non-null assertion operator !!

    non-null 확신 오퍼레이터

     

    변수를 non-null변수로 바꾼다.

    null체크를 하기 귀찮을 때나 잠깐 쓰겠지만 !!의 사용은 별로 좋지 못한 것 같다.

    fun main() {
    
        var str: String? = null
        str!! //str을 non-null타입으로 바꾼다.
        println(str.length) //ok
        //그러나 Runtime에러(NPE)가 발생할 것이다.
    }

    NPE가 발생하면 str.length부분이 아닌 !! 오퍼레이터가 사용된 str!! 코드 줄라인에 NPE를 띄워준다.

     


     safe cast - as?

     

    as는 다른 타입으로 캐스팅하는 연산자이다. as를 사용하면서 캐스팅이 불가한 경우 예외가 발생하는데 

    as에 ?를 달아 as?로 safe cast로 만들면 casting이 불가능한 경우 예외 대신 null을 리턴한다.

     

    fun main() {
        val tString: String = "kotlin"
        val tInt: Int = tString as Int //Runtime에서 ClassCastException발생
    
        val tString2: String = "kotlin"
        val tInt2: Int = tString2 as? Int 
        //컴파일 에러- smart cast as?의 리턴형은 Int?
        //Int?형 tInt2선언해야함
    
        val tString3: String = "kotlin"
        val tInt3: Int? = tString3 as? Int //ok
        val tInt4 = tString3 as? Int //ok
        // inferred type(암시적 타입 추론) 으로 tInt4는 Int?형으로 된다.
        
    }

    (추가) is 연산자를 사용하면서 safe cast를 사용하다 내가 잘못 이해하고 있었던 내용을 추가

    safe cast를 사용시 캐스팅에 실패하였다면 단지Int?형인 null을 리턴하는데

    그 값을 다른 임시 변수에 담을 수 있는 것이다. 

    tString3가 Int?형인 null로 캐스팅되는 것이 아니다.

     


    Collections of a nullable type

    collection의 element에 null인 놈이 껴있을 때 collection의 filterNotNull를 사용하여

    non-null인 element만 들어있는 List<T>를 뽑을 수 있다.

    fun main() {
        val nullableList: List<Int?> = listOf(1, 2, null, 4, null, 8, null, 11)
        val intList: List<Int> = nullableList.filterNotNull()
        for (item in intList) {
            println(item)
        }
    }

     

    댓글

Designed by Tistory.