본문 바로가기

코딩 이야기/안드로이드&코틀린 공부

[안드로이드&코틀린 공부] Nullable, Nonnull, Kotlin scope function

반응형

 

Nullable & Non-null

자바에서는 Null 값을 갖고 있는 객체/변수를 호출할 때 NullPointerException이라는 에러가 발생합니다.

코틀린에서는 Nullable과 Non-null로 변수를 선언할 수 있습니다.

기본적으로 모든 변수는 기본적으로 Non-null입니다.

즉 그냥 선언한 변수는 null을 담으면 에러가 발생합니다.

하지만 Nullable을 이용하면 변수에 null을 담을 수 있게 됩니다.

var nullable: String? = null

이를 통해 사후에 NullPointerException이 발생하는 걸을 미리 방지하는 역할을 할 수 있습니다.

변수를 Nullable로 선언하는 방법은 타입뒤에 ‘?’를 붙이면 됩니다.

이렇게 코틀린에는 Null을 처리해 에러의 빈도를 낮추는 다양한 방법이 있습니다.

?. 연산자

?. 연산자를 통해서 앞의 변수 혹은 객체가 null인 경우 함수 실행을 하지 않고, null이 아닐 경우에만 함수를 실행합니다.

public class A constructor(a: Int){
    var a : Int
    init{this.a = a}
    fun adder(b:Int){
        println(a+b)
    }
}

fun main() {
    var c = A(3)
    c?.adder(2)
}

이 예시는 c가 null이 아니기 때문에 정상적으로 실행이 됩니다.

이 연산자도 . 연산자 같은 느낌에 기능이 추가된거라 앞의 변수나 객체를 통해 접근할 수 있는 함수만 불러올 수 있습니다.

fun adder(a : Int){
    println(a+3)
}

fun main() {
    var c = 2
    c?.adder(2)
}

즉 이런건 불가능합니다.

?: 연산자

elvis 연산자라고도 부르는 연산자입니다. 엘비스 프레슬리 머리를 닮아서 그렇다는데 그런 것 같기도하고 잘 모르겠습니다.

간단히 요약하자면 ?: 기준으로 좌항에 있는 피연산자가 null이면 우항에 있는 피연산자를 실행하고, 우항에 있는 피연산자가 null이면 좌항에 있는 피연산자를 실행합니다.

var nullable: String? = null
var nonNullable: String = nullable ?: "nonNull"

위 코드의 경우 좌항의 피연산자인 nullable이 null이기 때문에 nonNullable에는 “nonNull”이 담기게 됩니다.

이를 .?과 응용하여서 다양한 형태로 구사 가능합니다.

val connectionUrl = Sqlins?.sqlurl ?: throw IllegalArgumentException("There is no url")

이렇게 해주면 Sqlins의 null 여부도 체크하면서 sqlurl이 존재하지 않을 경우 예외처리 해버릴 수 있습니다.

!!연산자

!!연산자는 non-null 단언 연산자입니다.

이름 그대로 피연산자를 null이 아니라고 단정짓게 됩니다.

컴파일 과정에서 null이여도 null이 아니라고 받아들여 컴파일 자체는 잘 진행되나, 실제 null일 경우 NullPointerException이 발생합니다.

println("first output") //출력 x
var x: String? = null
println(x.length) //출력 x

이 코드에서는

error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

이러한 에러가 발생합니다. 즉 컴파일러에서 null인것을 확인하고 실행조차 막아버립니다. 그렇기 때문에 앞선 코드인 first output 조차 출력이 되지 않습니다.

println("first output") //출력 : first output
var x: String? = null
println(x!!.length) // 출력 x

하지만 이 코드에서는 컴파일 단계에서는 문제가 발견되지 않기 때문에, 실행을 하게 되고 first output을 출력 후 NullPointerException에 걸리게 됩니다.

Kotlin Scope Function

스코프 함수는 이름 그대로 임시적인 범위를 형성하는 함수입니다.

좀 더 정확히는 객체에 대한 작업을 블록 안에 넣어서 실행할 수 있도록 합니다.

이 블록이 특정 객체에 대한 작업의 범위가 되는 것입니다.

스코프 함수는 범위 지정함수, 수신 객체 지정 함수라고도 합니다.

코틀린에서 제공하는 스코프 함수는 총 5가지로 구성되어있습니다.

  • let
  • run
  • with
  • apply
  • also

이 함수들은 접근하는 키워드와 리턴하는 값들로 구분지을 수 있습니다.

참조 키워드 리턴 값 확장함수인가?

apply this 객체(context) Y
run this 람다식 Y
with this 람다식 N
let it 람다식 Y
also it 객체(context) Y

apply

apply는 수신 객체의 내부 프로퍼티를 변경한 후 반환하기 때문에 초기화하는데에 주로 사용됩니다.

람다식의 수신객체가 apply의 수신객체이기 때문에 수신객체에 대한 명시가 필요치 않다.

public class A{
    var cnt : Int = 2
}

fun main() {
    var a = A().apply{
        cnt = 3
    }
}

apply를 사용하지 않는다면 이렇게 작성해야한다.

public class A{
    var cnt : Int = 2
}

fun main() {
    var a = A()
    a.cnt = 3
}

run

run은 apply와 매우 비슷한 기능으로 동작합니다. 다만 차이점 리턴값이 수신 객체가 아닌 run 범위 안의 마지막 줄이 리턴됩니다.

즉 수신객체가 아니라 수신객체에 대한 동작 후 결과값을 리턴받아야할 때 사용합니다.

public class A{
    var cnt : Int = 2
    fun isoverfive(): Boolean{
        if(cnt > 5){
            return true
        }else{
            return false
        }
    }
}

fun main() {
    var a = A().run{
        cnt = 6
        isoverfive()
    }
    println(a) //출력 : true
}

또한 리턴할 부분을 지워버리면 Unit이 리턴됩니다.

public class A{
    var cnt : Int = 2
}

fun main() {
    var a = A().run{
        cnt = 6
    }
    println(a) //출력 : Kotlin unit
}

with

with은 수신객체를 파라미터로 받는 run이라 생각하시면 편합니다. 기능적으로 run과 동일하기 때문에 run이 깔끔해서 많이 사용하고 with은 잘 쓰지 않는다고 합니다.

public class A{
    var cnt : Int = 2
    fun isoverfive(): Boolean{
        if(cnt > 5){
            return true
        }else{
            return false
        }
    }
}

fun main() {
    var a = A()
    var b = with(a){
        cnt = 6
        isoverfive()
    }
    println(b) //출력 : true
}

run도 마찬가지지만 with은 그냥 변수에 리턴 없이 단독으로 사용하여 객체의 프로퍼티를 변경만 하는 용도로도 사용가능합니다.

var a = A()
with(a){
    cnt = 6
}
println(a) //출력 : true

let

let은 객체가 null이 아닌 경우에 실행해야 할 때 사용합니다.

이러면 앞서 공부한 !. 연산자하고 크게 다를게 없어보입니다. 하지만 여기에 객체를 it을 통해 접근하는데에 의미가 있습니다.

public class A{
    var cnt : Int = 7
    fun isoverfive(): Boolean{
        if(cnt > 5){
            return true
        }else{
            return false
        }
    }
}

fun main() {
    var a = A()
    a.let{
        println("hello world")
    }
}

also

also는 기존 객체를 수정하거나 변경하지 않고, 디버깅을 위한 로깅 등의 추가적인 부가 작업을 하려고 할 때 사용한다고 합니다. 즉 확인을 위한 출력문이나 이런 용도로만 사용되는 것 같습니다.

public class A{
    var cnt : Int = 7
    fun isoverfive(): Boolean{
        if(cnt > 5){
            return true
        }else{
            return false
        }
    }
}

fun main() {
    var a = A()
    a.let{
        println("hello world")
    }.also{
        println(a.cnt)
    }
}

당연히 확장함수들은 이런식으로 다른 확장함수들과 중첩해서 사용하는 것이 가능합니다.

also는 apply 처럼 수신 객체를 반환 하므로 블록 함수가 다른 값을 반환 해야하는 경우에는 also 를 사용할수 없습니다.

it & this

public class A{
    var cnt : Int = 7
    fun isoverfive(): Boolean{
        if(cnt > 5){
            return true
        }else{
            return false
        }
    }
}

fun main() {
    var cnt : Int = 10
    var a = A()
    a.let{
        println(cnt)
    }
}

let을 사용한 예제인데 이 상황에서 cnt를 출력하면 어떻게 될까?

아쉽게도 10이 출력됩니다. 전 스코프 함수 범위 내에 있으니까 a.cnt처럼 될 줄 알았는데 아니였습니다.

이럴때 객체의 a와 main 함수의 a를 구분짓기 위해 it 키워드를 사용해야 합니다.

a.let{
        println(it.cnt)
    }

위의 표대로 apply나 run이나 with은 this 키워드를 사용해야겠죠?

반응형