코틀린 & 컴포즈 & Java/코틀린인액션

코틀린 타입 종류 Null가능성

코딩하는후운 2023. 3. 27. 13:56
반응형

타입 시스템

  • 코틀린의 타입 시스템은 코드의 가독성을 향상시키는데 도움이되는 몇가지 특성을 새로 제공

널 가능성 (nullability)

  • NullPointerException오류를 피할 수 있게 돕기 위한 코틀린 타입 시스템의 특성
  • null에 대한 접근 방법을 실행시점 → 컴파일 시점

널이 될 수 있는 타입 (nullable type)

  • 타입 시스템이 널이 될수 있는 타입을 명시적으로 지원
  • 타입 이름 뒤에 물음표(?)를 명시
fun strLenSafe(s: String?) = ...
  • null과 비교하고 나면 컴파일러는 그 사실을 기억하고, 해당 값을 널이 될 수 없는 타입의 값처럼 사용할 수 있다.
fun strLenSafe(s: String?) : Int =
    if(s != null) s.length else 0

자바에서 NullPointerException 오류를 다루는 방법

  • 애노테이션을 통해 @Nullable 이나 @NotNull 널 여부 표시를 할 수 있다.
    • 자바 컴파일 절차가 아니기 때문에 일관성 적용 보장 X
    • 모든 코드에 추가하는 일도 쉽지 않다.
  • 자바8에 도입된 Optional 타입 등 null을 감싸는 래퍼 타입 활용
    • 코드가 더 지저분해지고 실행 시점에 성능이 저하된다.

종합적인 해법 → 코틀린의 널이 될 수 있는 타입

안전한 호출 연산자 (?.)

  • ?. 연산자는 null검사와 메서드 호출을 한번에 연산으로 수행
if (s != null) s.toUpperCase() else null

==

s?.toUpperCase()
  • 호출하려는 값이 null이면 호출은 무시되고 null이 결과 값이 된다.
  • 결과 타입은 String?이다.
  • 메서드 호출뿐만 아니라 프로퍼티를 읽고 쓸때도 사용 가능.
val contry = this.company?.address?.country <- 연쇄해 사용 가능

엘비스 연산자 (?:)

  • null 대신 사용할 디폴트 값을 지정할 때 사용
fun strLenSafe(s: String?) : Int =
    s?.length ?: 0
  • 코틀린에서는 return이나 throw 등의 연산도 식이다.
    • 엘비스 연산자의 우항에도 넣을 수 있다.
val address = person.company?.address
	?: throw IllegalArgumentException("No address") <- 주소가 없으면 예외를 발생

with(address) {  //address는 널이 아니다.
	
}

안전한 캐스트 (as?)

  • 값을 대상 타입으로 변환할 수 없으면 null을 반환
val otherPerson = o as? Person?: return false <- 타입이 일치하지 않으면 false 반환

return otherPerson.firstName == firstName &&
					otherPerson.lastName == lastName
//안전한 캐스트를 하고 나면 Person타입으로 스마트 캐스트 된다.

널이 아님 (!!)

fun ignoreNulls(s: String?) {
	val sNotNull: String = s!!  <- 예외는 이 지점을 가리킨다.
	println(sNotNull.length)
}
  • s가 널이면 NPE발생
  • 예외는 null값을 사용하는 코드가 아니라 단언문이 위치한 곳을 가리킨다.

!!를 널에 대해 사용해서 발생하는 예외 스택 트레이스에는 어떤 파일의 몇 번째 줄인지에 대한 정보는 들어있지만, 어떤 식에서 예외가 발생했는지에 대한 정보는 들어있지 않다.

여러 !! 단언문을 한줄에 함께 쓰지마라!

person.company!!.address!!.contry

let 함수

  • 널이 될 수 있는 값을 널이 아닌 값만 인자로 받는 함수에 넘기는 경우
fun sendEmailTo(email: String) {..}

val email: String? = ...
sendEmailTo(email)
>>ERROR!!!
if (email != null) sendEmailTo(email)
  • let함수는 자신의 수신 객체를 인자로 전달받은 람다에게 넘긴다.
    • 널이 될 수 있는 값에 안전한 호출 구문(?.)을 사용해 람다에게 널이 될 수 없는 타입을 let에 전달한다.
email?.let {
	email -> sendEmailTo(email)
}

여러 값이 널인지 검사해야 한다면 let호출을 중첩해서 처리할 수 있다. 하지만 코드가 복잡해져서 가독성 떨어짐.

그런경우 if를 사용해 모든값을 한꺼번에 검사하는 편이 낫다.

나중에 초기화할 프로퍼티

  • 널이 될 수 없는 타입이라면 반드시 널이 아닌 값으로 프로퍼티를 초기화 해야한다.
    • 널이 될 수 있는 타입을 사용해야한다.
    • 널 검사나 !! 연산자를 써야한다.
  • 이를 해결하기 위해 나중에 초기화(late-initialized) 할 수 있다.
    • lateinit 변경자를 붙인다.
private lateinit var myService: MyService // 초기화 하지 않고 널이 될 수 없는 프로퍼티 선언
  • 나중에 초기화 하는 프로퍼티는 항상 var이어야 한다.
  • val 프로퍼티는 final 필드로 컴파일 되며, 생성자 안에서 반드시 초기화 해야 한다.

널이 될 수 있는 타입 확장

예) String을 확장해 정의된 isEmpty와 isBlank

@kotlin.internal.InlineOnly
public inline fun CharSequence.isEmpty(): Boolean = length == 0

public actual fun CharSequence.isBlank(): Boolean = length == 0 || indices.all { this[it].isWhitespace() }
  • isNullOrEmpty 이나 isNullOrBlank메서드
    • 안전한 호출을 하지 않아도 된다.
@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrEmpty(): Boolean {
    contract {
        returns(false) implies (this@isNullOrEmpty != null)
    }

    return this == null || this.length == 0
}

@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrBlank(): Boolean {
    contract {
        returns(false) implies (this@isNullOrBlank != null)
    }

    return this == null || this.isBlank()  //두번째 this에는 스마트 캐스트가 적용된다.
}
  • 코틀린에서는 널이 될 수 있는 타입의 확장 함수 안에서는 this가 널이 될 수 있다는 점이 자바와 다르다.

타입 파라미터의 널 가능성

  • 타입 파라미터 T를 클래스나 함수 안에서 타입 이름으로 사용하면, 이름 끝에 물음표가 없더라도 T가 널이 될 수 있는 타입이다.
fun <T> printHashCode(t: T) {
	println(t?.hashCode()) <- 't'가 null이 될 수 있으므로 안전한 호출을 써야한다.
}
//T의 타입은 Any?로 추론된다.
  • 타입 파라미터가 널이 아님을 확실히 하려면 널이 될 수 없는 타입상한을 지정해야 한다.
fun <T: Any> printHashCode(t: T) //이제 T는 널이 될 수 없는 타입이다.

타입 파라미터는 널이 될 수 있는 타입을 표시하려면 반드시 물음표 타입 이름 뒤에 붙여야 한다는 규칙의 유일한 예외다.

플랫폼 타입

  • 코틀린이 널 관련 정보를 알 수 없는 타입.
  • 널 가능 or 널 불가능 아무 타입으로 처리 가능.
    • 책임은 나에게 있다.
  • 자바 타입은 코틀린에서 플랫폼 타입으로 표현 된다.
Person은 자바 클래스이다.

fun yellAt(person: Person) {
	println(person.name.toUpperCase())
}

yellAt(Person(null))
  • 코틀린 컴파일러는 공개(public) 가시성 코틀린 함수의 널이 아닌 타입인 파라미터와 수신객체에 대한 널 검사를 추가해준다. (= 컴파일만 통과 한다는 뜻인듯)
    • 공개 가시성 함수에 널 값을 사용하면 즉시 예외 발생.

269p ??

  • toUpperCase()가 수신객체로 널을 받을 수 없다는 예외가 발생한다고 하였는데 난 NPE발생함.
  • 널 값을 처리해 줌으로써 예외 발생 X
fun yellAt(person: Person) {
	println((person.name ?: "Anyone").toUpperCase())
}

코틀린이 왜 플랫폼 타입을 도입했는가? 모든 타입을 널이 될 수 있는 타입으로 다루면 널이 될 수 없는 값에 대해서도 불필요한 널 검사가 들어가기 때문. 검사에 드는 비용이 더 커진다.

val i: Int = person.name
ERROR: Type mismatch: inferred type is String! but Int was expected
  • 코틀린 컴파일러가 표시한 String!라는 타입은 자바 코드에서 온 타입이다.
  • ! 표기는 String! 타입의 널 가능성에 대해 아무 정보도 없다는 뜻

상속

  • 코틀린에서 자바 메서드를 오버라이드 할 때, 반환 타입을 널이 가능한지 불가능한지 결정해야한다.
  • 구현 메서드를 다른 코틀린 코드가 호출할 수 있으므로, 코틀린 컴파일러는 널이 될 수 없는 타입으로 선언한 모든 파라미터에 대해 널이 아님을 검사하는 단언문을 만들어 준다.
    • 메서드에 널 값을 넘기면 에러 발생
interface StringProcessor {
	void process(String value);
}
  • 코틀린 컴파일러는 두 구현을 다 받아들인다.
override fun process(value: String) {
}

override fun process(value: String?) {
}

 

반응형