Skils/Kotlin

[Kotlin] 널 가능성

재한 2022. 9. 1. 14:45

📕Null

  • 참조 값 중에서 아무것도 참조하지 않는 경우를 나타내는 특별한 참조값들
  • 여기서 참조는 어떤 할당된 객체도 가리키지 않는 참조를 뜻한다. 
  • 다른 참조와 비슷하게 동작하지 않는다. 

🔎자바 vs 코틀린

💻자바

모든 참조 타입의 변수에 널을 대입할 수 있지만, 이때 이 참조 타입(하지만 값은 널)에 정의된 메서드나 프로터 리를 사용하려고 하면 NullPointerException(NPE)가 발생. --> 런타임에 프로그램을 실행해봐야 오류를 찾을 수 있는 최악의 오류

💻코틀린

널 값이 될 수 있는 참조 타입과 널 값이 될 수 없는 참조타입을 구분 해고 널 발생 여부를 컴파일 시점으로 옮겨주기 때문에  NullPointerException(NPE) 예외를 상당 부분 막을 수 있다.

 

📗널이 될 수 있는 타입

코틀린 타입 시스템의 중요한 특징은 널 값을 포함하는 타입과 그렇지 않은 타입을 구분하는 능력이다.

자바-> 모든 참조 타입이 날이 될 수 있기에, 컴파일러는 참조 타입의 변수가 null 값이 아닌 값만 포함한다는 사실을 보장 x

코틀린-> 날이 될 수 있는 참조 타입이 정해져 있다. String 같은 타입에 null값을 대입할 수는 없다.

fun main(){
    println(isLetterString("abc"))
    println(isLetterString(null)) //null값은  String에 적절한 value가 아니다.
}
fun isLetterString(s: String) : Boolean {
    if(s.isEmpty()) return false
    for(ch in s){
        if(!ch.isLetter()) return false
    }
    return true
}

 

만약 널이 될 수도 있는 값을 받는 함수를 작성하려면 파라미터 타입 뒤에 물음표(?)를 붙여서 타입을 널이 될 수 있는 타입으로 지정해야 한다.

fun isBooleanString(s:String?)=s=="false"||=="true"

String? 같은 타입은 널이 될 수 있는 타입이라 불린다.

모든 날이 될 수있는 타입은 원래 타입에 속하는 모든 값으로 이뤄진 집합을 null로 확장한 집합이 값의 집합이 된다.

  • 널이 될 수 있는 타입의 변수에 항상 널이 될 수 없는 타입의 값을 대입할 수 있다. 
  • 널이 될 수 없는 타입의 변수에 널이 될 수 있는 타입의 값을 대입할 수는 없다.
fun main(){
    println(isBooleanString(null)) //Ok
    val s : String? = "abc"  //Ok
    val ss: String = s //Error
}

 

Int나 Boolean 같은 원시 타입도 널이 될 수 있는 타입이 존재한다. 하지만 원시 타입의 널이 될 수 있는 타입은 항상 박싱 한 값만 표현한다.

여기서 박싱이란 값 형식을 참조 형식으로 변환해주는 것을 의미한다.

  • 값 형식 : 해당 데이터를 직접적으로 스택(stack) 메모리에 저장함
  • 참조 형식은 힙(heap) 메모리에 데이터를 저장함.

⚡Nothing?

  • 가장 작은 날이 될 수 있는 타입
  • 널 상수 이외의 어떤 값도 포함하지 않음.
  • null값 자체이며 다른 모든 널이 될 수 있는 타입의 하위 타입이다.

⚡Any?

  • 가장 큰 날이 될 수 있는 타입
  • 코틀린 타입 시스템 전체에서 가장 큰 타입
  • 널이 될 수 있는 모든 타입과 널이 될 수 없는 모든 타입의 상위 타입이다.

 

💻날이 될 수있는 타입은 원래 타입에 들어있는 어떤 프로퍼티나 메서도드 제공하지 않는다.

예를 들어  s: String?이라면 s.isEmpty()등 메서드나 프로퍼티를 사용하지 못함.

 

📗널 가능성과 스마트 캐스트

널이 될 수있는 값을 처리하는 가장 직접적인 방법은 해당 값을 조건문을 사용해 null과 비교하는 것이다.

fun isLetterString(s: String?) : Boolean {
    if(s==null ) return false 
    //만약 s가 null이면 for 문을 가기전에 return 되므로 for문에 들어갈 수 있는 s는 절대로 null값이 될 수 없다. 
    for(ch in s){
        if(!ch.isLetter()) return false
    }
    return true
}

원래라면 널이 될 수 있는 타입은 for문과 String에 대한 메서드나 프로퍼티를 사용하지 못하지만,  null에 대한 검사를 추가하면 컴파일된다. 

왜 이런 일이 발생할까?

그것은 코틀린에서 스마트 캐스트라고 불리는 기능이 컴파일을 가능하게 해 준다.

위 코드를 예를 들어서 쉽게 설명하자면 if(s==null) 다음 문장은 return으로 끝나기 때문에 만약 s가 null일 경우 return 문으로 끝나기 때문에 만약 다음 if문 다음 문장이 실행될 경우 변수 s[널이 될수 있는 값]를 널이 될 수 없는 String 타입으로 변환하고, 나머지 함수 본문을 실행한다.

 

스마트 캐스트를 실행하려면 대상 변수의 값이 검가 지점과 사용 지점 사이에서 변하지 않는다고 컴파일러가 확신할 수 있어야 한다. 만약 널 검사와 사용 지점 사이에서 값이 변경되는 경우에는 스마트 캐스트가 작동하지 않는다.

var s =readLine()
    if(s!=null){
        s=readLine()
        println(s.length())
    }

 

📗널 아님 단언 연산자

readLine() 함수와 관련해!! 연산자를 이미 살펴봤다.!! 연산자는 널 아님 단언이라고 부르는데, KotlinNullPointerException 예외를 발생시킬 수 있는 연산자다.

이 연산자가 붙은 식의 타입은 원래 타입의 날이 될 수 없는 버전이다.

널 값을 역참조 하려 할 때 예외를 던지는 동작을 부활시킴.

 

📗안전한 호출 연산자

날이 될 수 있는 타입의 값에 대해서는 그에 상응하는 널이 될 수 없는 타입의 값에 있는 메서드를 사용할 수 없다고 이미 설명했다. 하지만 특별한 안전한 호출 연산을 사용하면 이런 제약을 피할 수 있다.

fun readInt() = readLine()?.toInt()
fun readInt(): Int?{
	val tmp= readLine()
    
    return if(tmp!=null) temp.toInt() else null
}

두 함수는 같은  함수이다.

안전한 호출 연산자는 수신 객체가 날이 아닌 겨웅 일반적인 함수 호출 처럼 작동한다.

-->  수신 객체가 날이 아닌 경우에는 의미있는 일을 하고, 수신 객체가 널인 경우에는 벌을 반환하라!

📗널 복합 연산자

  • 널이 될 수 있는 값을 다룰 때 유용한 연산자
  • 형태는?:
  • 엘비스 연산자를 사용하면 널을 대신할 디폴트 값을 지정할 수 있다.
  • 엘비스 연산자라고도 부른다.
fun main(){
    sayHello("John")
    sayHello(null)
}
fun sayHello(name : String?) {
    println("Hello, " + (name?: "Unknown"))
}

이 연산자의 결과는 왼쪽 피연산자가 날이 아닐 경우에는 왼쪽 피연산자의 값이고, 왼쪽 피연산자가 벌일 경우에는 오른쪽 피연산자의 값이다. 

fun sayHello(name : String?){
	println("Hello ",+(if(name!=null) name else "Unknown"))

두 sayHello는 같은 함수이다.

 

안전한 연산과 엘비스 연산자를 조합해서 수신 객체가 널 일 때의 디폴트 값을 지정할 수 있다.

val n = readLine()?.toInt() ?: 0

위 코드는 프로그램의 표준 입력이 널을 반환할 경우 0을 n에 반환한다.

 

엘비스 연산자 오른쪽에는 return이나 throw 같은 제어 흐름을 깨는 코드도 넣을 수 있다.

val currentName = name?: return "Unknown"

✔정리

  • 코틀린은 자바와 달리 널 값이 될 수 있는 참조 타입을 확실히 구분해주기에 NPE 예외를 상당 부분 막을 수 있다.
  • 날이 될 수도 있는 타입을 만들려면 파라미터 타입 뒤에 물음표를 붙여서 지정해준다.
    • 널이 될 수 있는 타입의 변수에 널이 될 수 없는 타입의 값을 대입은 가능.
    • 널이 될 수 없는 타입의 변수에 널이 될 수 있는 타입의 변수는 대입 불가능.
    • 날이 될 수 있는 타입은 원래 타입(물음표 붙이기 전)에 들 어있는 어떤 프로퍼티나 메서드를 사용할 수 없다.
  • 스마트 캐스트는 날이 될 수있는 값을 처리하는 기능을 한다.
    • null에 대한 검사를 한 뒤의 코드는 확실하게 null이 될 수 없다는 정보를 가지고 있기에, 널이 될 수 있는 값을 널이 될 수 없는 값으로 타입 변환한다.
    • 스마트캐스트를 실행하기 위한 조건은 대상 변수의 값이 검사 지점과 사용 지점 사이에서 변하지 않는다고 컴파일러가 확실한 수 있어야 하기에 중간에 값이 변경되는 경우에는 스마트캐스트가 작동하지 않는다.
    • 가변(var)프로퍼티에는 절대 스마트캐스트를 적용할 수 없다.
  • 안전한 호출 연산자
    • 뒤에 물음표(?)를 붙이면 된다.
    • 수신 객체가 날이 아닌 경우에는 의미 있는 일을 하고, 수신 객체가 벌일 경우에는 널을 반환함.
  • 엘비스 연산자
    • 형태는 ?:
    • 널을 대신할 디폴트 값을 지정할 수 있다.