-
[코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.2 널 가능성)코틀린 공부/코틀린 2023. 12. 29. 17:31
4.2 널 가능성
자바와 마찬가지로 코틀린은 참조 값에는 아무것도 참조하지 않는 경우를 나타내는 특별한 null이라는 값이 있다. 자바에서는 모든 참조 타입의 변수에 null을 대입할 수 있지만, 이때 이 참조 타입에 정의된 메서드나 프로퍼티를 사용하려면 NPE(NullPointerException)이 발생한다.
이 오류가 최악인 이유는 컴파일러가 정적인 타입 정보만으로는 이런 오류를 잡아낼 수 없어서 런타임에 프로그램을 실행해봐야 이 오류를찾을 수 있기 떄문이다.
코틀린 타입 시스템에는 null 값이 될 수 있는 참조 타입과 없는 타입을 확실하게 구분해주는 큰 장점이 있다. 이 기능은 null 발생 여부를 컴파일 시점으로 옮겨주기 때문에 악명 높은 NullPointerException 예외를 상당 부분 막을 수 있다.
4.2.1 널이 될 수 있는 타입
코틀린에서 기본적으로 모든 참조 타입은 널이 될 수 없는 타입이다. 그래서 String 같은 타입은 null 값을 대입할 수 없다.
fun isLetterString(s:String): Boolean{ if(s.isEmpty()) return false for(ch in s){ if(!ch.isLetter()) return false } return true }
println(isLetterString("abc")) println(isLetterString(null)) // Null can not be a value of a non-null type String
코틀린에서 널이 될 수도 있는 값을 받는 함수를 작성하려면 파라미터 타입 뒤에 물음표(?)를 붙여서 타입을 널이 될 수 있는 타입으로 지정해야 한다.
fun isBooleanString(s: String?) = s == "false" || s == "true"
코틀린에서 String? 같은 타입은 널이 될 수 있는 타입 (nullable type)이라고 한다.
타입 시스템용어에서 모든 널이 될 수 있는 타입은 원래 타입의 상위 타입이다.
널이 될 수 있는 타입의 변수에 항상 널이 될 수 없는 타입의 값을 대입할 수 있고, 하지만 반대로 널이 될 수 없는 타입의 변수에 널이 될 수 있는 타입의 값을 대입할 수는 없다.
val s:String? = "abc" // OK val ss:String = s // Type mismatch.
널이 될 수 있는 타입은 원래 들어있는 어떤 프로퍼티나 메서드도 제공하지 않는다. 널이 될 수 있는 타입은 코틀린의 확장 메커니즘을 활용해 자체적인 메서드와 프로퍼티를 제공한다.
4.2.2 널 가능성과 스마트 캐스트
널이 될 수 있는 값을 처리하는 가장 직접적인 방법은 조건문을 이용해 null과 비교하는 것이다.
fun isLetterString(s:String?): Boolean{ if(s == null) return false if(s.isEmpty()) return false for(ch in s){ if(!ch.isLetter()) return false } return true }
위에처럼 null 검사를 추가하는 것을 스마트 캐스트라는 기능이다. 기복적으로 null에 대한 동등성 검사를 수행하면, 컴파일러는 코드 흐름의 가지 중 한쪽에서는 대상 값이 확실히 널이고 다른 가지에서는 확실히 널이 아니라는 사실을 알 수 있다. 그 후 컴파일러는 이 정보를 사용해 값 타입을 세분화함으로써 널이 될 수 있는 값을 널이 될 수 없는 값으로 타입 변환(cast)한다. 이런 기능을 스마트 캐스트 라고 한다.
(스마트 캐스트는 널 가능성에만 제한되지 않는다)
when에서도 적용 가능.
fun describeNumber(n: Int?) = when (n) { null -> "null" in 0..10 -> "small" else -> "big" }
4.2.3 널 아님 단언 연산자
!! 연산자는 널 아님 단언 이라고 부르는데, KotlinNullPointerException 예외를 발생시킬 수 있는 연산자다. 이 연산자가 붙은 식의 타입은 널이 될 수 없는 버전이다.
기본적으로 널 아님 단언은 자바 프로그램의 널 관련 동작, 널 값을 역참조하려 할 때는 예외를 던지는 동작을 부활시킨다.
val n = readln().toInt()
일반적으로 널이 될 수 있는 값을 사용하려면 그냥 예외를 던지는 방식보다 더 타당한 응답을 제공해야 하기 때문에 이 연산자를 사용하지 말아야 한다. 하지만 이 연산자 사용을 정당화할 수 있는 경우가 있다.
fun nullAssert(){ var name: String? = null fun initialize(){ name = "John" } fun sayHello(){ println(name!!.uppercase()) } initialize() sayHello() }
이 경우 이름에 널이 될 수 없는 값이 할당된 다음에 sayHello() 함수가 호출되므로 널 아님 단언도 적절한 해법이다. 하지만 이와 같은 경우라도 널을 다룰 때 쓸 수 있는 덜 무딘 도구를 사용하거나 코드 제어 흐름을 고쳐 써서 컴파일러가 스마트 캐스트를 적용할 수 있게 하는 편이 더 낫다.
그래도 스마트 캐스트 추천
4.2.4 안전한 호출 연산자
4.2.2에서 널이 될 수 있는 타입의 값에 대해서는 그의 상응하는 널이 될 수 없는 타입의 값에 있는 메서드를 사용할 수 없다고 이미 설명했다. 하지만 특별한 안전한 호출 연산(safe call)을 사용하면 이런 제약을 피할 수 있다.
안전한 호출 연산자를 사용하면 다음 형태로 코드를 다시 작성할 수 있다.
fun readInt() = readLine()?.toInt()
위에 함수는 풀어서 쓰면 아래 함수와 같다.
fun readInt(): Int? { val tmp = readLine() return if (tmp != null) tmp.toInt() else null }
안전한 호출 연산자는
- 널이 아닐경우면 readLine().toInt() 처럼 작동하고
- 널이면 null을 돌려준다.
'수신 객체가 널이 아닌 경우에는 의미 있는 일을 하고, 수신 객체가 널인 경우에는 널을 반환해라'
4.2.5 엘비스 연산자
널이 될 수 있는 값을 다룰 때 유용한 연산자로 널 복합 연산자(null coalescing operator)인 '?:' 을 들수 있다.
이 연산자를 사용하면 null 대신 default 값을 지정할 수 있다.
fun sayHello(name: String?) { println("Hello, ${name ?: "Unknown"}") }
sayHello("John") // Hello, John sayHello(null) // Hello, Unknown
이 연산자의 결과는 왼쪽 피연산자가 널이 아닐 경우에는 왼쪽 피연산자의 값이고, 널일 경우 오른쪽 피연산자의 값이다.
fun sayHello(name: String?) { val n = readLine() }
- name != null 이면 n = name
- name == null 이면 "empty"
더 간단한 패턴으로, return이나 throw 같은 제어 흐름을 깨는 코드를 엘비스 연산자 오른쪽에 넣는 방법도 있다.
class Name(val firstName: String, val lastName: String?) class PersonA(val name: Name?) { fun describe(): String{ val currentName = name ?: return "Unknown" return "${currentName.firstName} ${currentName.lastName}" } }
println(PersonA(Name("John", "Lee")).describe()) // John Lee println(PersonA(null).describe()) // Unknown
우선순위 면에서는 엘비스 연산자는 or 등의 중위 연산자와 in, !in 사이에 위치한다. 특히 비교/동등성 연산자나 ||, &&, 대입 보다 우선순위가 높다.
'코틀린 공부 > 코틀린' 카테고리의 다른 글
[코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.4 객체) (1) 2023.12.30 [코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.3 단순한 변수 이상 인 프로퍼티) (0) 2023.12.30 [코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.1 클래스 정의하기) (0) 2023.12.29 [코틀린 완벽 가이드] 3장 함수 정의하기 (3.5 예외 처리) (0) 2023.12.29 [코틀린 완벽 가이드] 3장 함수 정의하기 (3.3 조건문와 3.4 루프) (0) 2023.12.29