본문 바로가기

Study/Kotlin Study

[Kotlin] 널 가능성

+ 항공대학교 김철기 교수님의 객체 지향 프로그래밍 과목 내용를 정리한 글입니다.

널 (NULL)

null은 아무 것도 참조하지 않는 참조 값의 특별한 상태이다.

null에 대해서 멤버 접근을 시도하면 NullPointerException이 발생한다.

(컴파일 시간에 파악이 되지 않아 최악의 에러 중 하나로 간주)

Kotlin에서 일반 참조형에는 null을 담을 수 없다. (에러 발생 => NullPointer Exception 예방)

// 문자로 이루어진 문자열인지 확인하는 함수
fun isLetterString(s:String): Boolean {
    if (s.isEmpty()) return false // 빈 문자열이라면 false
    for (ch in s) { // 문자가 아니라면
        if (!ch.isLetter()) return false // 문자가 아니라면 false
    }
    return true
}

fun main() {
    println(isLetterString("abc"))
    println(isLetterString(null)) // 컴파일 에러 발생
}

널이 될 수 있는 타입 (nullable)

참조자의 타입 뒤에 ?를 붙인다.

// s가 "false"나 "true"라면 true를 반환하는 함수
fun isBooleanString(s:String?) = s == "false" || s == "true"


fun main() {
    println(isBooleanString(null)) // ok
    val s:String? = "abc" // ok
    val ss:String = s // Error: type mismatch
}

널이 될 수 있는 타입에 대한 멤버 접근

널이 될 수 있는 타입은 기본 타입의 멤버 접근 등의 연산을 바로 수행하려 할 경우 컴파일 에러가 발생한다.

fun isLetterString(s:String?): Boolean {
    if (s.isEmpty()) return false // Error

    for (ch in s) { // Error
        if (!ch.isLetter()) return false
    }
    return true
}

널 가능성 제거를 통한 스마트 캐스트

프로그램 흐름에서 null인 경우가 논리적으로 배제되면 컴파일러는 이를 인지하고 널 가능성을 제거하고 컴파일한다.

fun isLetterString(s:String?): Boolean {
    if (s == null) return false
    if (s.isEmpty()) return false // Error

    for (ch in s) { // Error
        if (!ch.isLetter()) return false
    }
    return true
}

조건문 내에서의 스마트 캐스트

fun describeNumber(n: Int?)
    = when {
        // range를 사용할 경우는 null인 경우를 처리하지 않아도 정상작동한다.
        n == null -> "null" // null인 경우 처리
        n >= 0 && n <= 10 -> "Small"
        n > 10 && n <= 100 -> "Large"
        else -> "Out of range"
    }
    
fun isSingleChar(s: String?)
    = s != null && s.length == 1
    // && 연산은 앞 조건에서 false라면 뒤 조건은 보지 않는다.
    // || 연산은 앞 조건에서 true라면 뒤 조건은 보지 않는다.
    // 따라서 null을 인자로 받아도 오류가 발생하지 않는다.

fun main() {
    println(describeNumber(null))
    println(isSingleChar(null))
}

객체의 가변 프로퍼티와 스마트 캐스트

객체의 가변 프로퍼티는 스마트 캐스트가 불가능하다.

=> 프로그램 다른 어떤 곳에서 갑자기 값을 바꿀 수 있기 때문이다. (일말의 가능성 존재)

class MyString {
    var str: String? // 가변 프로퍼티

    fun isStrEmpty(): Boolean {
        if (str == null) return true
        if (str.isEmpty()) return true // Error 발생
        else return false
    }
}

 fun main() {
     val s = MyString()
     s.str = "Hello"
     println(s.isStrEmpty()) 
 }

local 변수로 저장하면 스마트 캐스트가 가능하다.

class MyString {
    var str: String? // 가변 프로퍼티

    fun isStrEmpty(): Boolean {
        val s = str // local 변수로 저장
        if (s == null) return true
        if (s.isEmpty()) return true
        else return false
    }
}

 fun main() {
     val s = MyString()
     s.str = "Hello"
     println(s.isStrEmpty())
 }

널 아님 단언 연산자 (!!)

널이 될 수 있는 타입에 대하여 프로그래머가 널이 아니라고 확언해주는 연산자

 

- 그럼에도 불구하고 널이었을 경우 kotlinNullPointerException이 발생한다.

- 널 아님 단언 연산자는 반드시 필요한 경우를 제외하고는 사용하지 말아야 한다.

fun readInt() = readLine()!!.toInt() 
// readLine()은 null을 반환할 수 있다.
// 피할 수 있으면 피하는 것이 좋다. 
// 반환형은 Int. 널이 될 수 없다.

 

따라서 아래 코드를 선호해야한다.

fun readInt() = readLine()?.toInt()
// 널이 아니라면, 널이 아닌 값에 대해 뒤 연산을 실행한다. (반환 타입: Int?)
// 널이라면, 뒤 연산을 하지 않고 널을 반환한다.

엘비스 연산자

연산자 왼쪽의 값이 null인 경우 연산자 오른쪽 값으로 치환한다.

fun readInt() = readLine()?.toInt() ?: 0
// readLine()이 null인 경우 readLine?.toInt() 값이 null이 된다.
// 이 때, null을 ?: 0이 0으로 치환한다.
// 결과적으로 readInt()는 null을 반환할 가능성이 없어지므로 반환형이 Int가 된다.
fun sayHello(name: String?) {
    println("Hello, ${name ?: "Unknown"}") // null이라면 "Unknown 반환
}

 fun main() {
     sayHello("Charlie")
     sayHello(null)
 }

엘비스 연산자 우측의 return / throw

엘비스 연산자도 일종의 조건문이므로 연산자 우측에 값 대신 return이나 throw를 둘 수 있다.

fun readInt() = readLine()?.toInt() ?: 0
// readLine()이 null인 경우 readLine?.toInt() 값이 null이 된다.
// 이 때, null을 ?: 0이 0으로 치환한다.
// 결과적으로 readInt()는 null을 반환할 가능성이 없어지므로 반환형이 Int가 된다.


class Name(val firstName: String, val familyName: String?)

class Person(val name: Name?) {
    fun describe(): String {
        val currentName = name ?: return "Unknown" // null일 경우 decribe함수는 "Unknown" 반환
        return "${currentName.firstName} ${currentName.familyName}"
    }
}

 fun main() {
     println(Person(Name("John", "Doe")).describe()) // John Doe
     // John Doe라는 이름을 갖는 Name 객체를 Person 생성자의 인자로 전달
     println(Person(null).describe()) // Unknown
 }

'Study > Kotlin Study' 카테고리의 다른 글

[Kotlin] 객체 (Object)  (0) 2023.09.17
[Kotlin] 단순한 변수 이상인 프로퍼티  (0) 2023.09.17
[Kotlin] 함수  (4) 2023.09.03
[Kotlin] 클래스 정의하기  (1) 2023.08.28
[Kotlin] 예외 처리  (0) 2023.08.25