프로그래밍 노트/Kotlin

[kotlin] 코틀린 타입 시스템

깡냉쓰 2023. 3. 25. 11:44
728x90
반응형

엘비스 연산자(?:)

  • 코틀린은 엘비스 연산자(?:)를 사용하여 디폴트 값을 편하게 지정할 수 있다.
fun foo(s: String?) = s ?: ""
fun strLenSafe(s: String?): Int = s?.length ?: 0
  • return이나 throw등의 연산도 식이니, 엘비스 연산자의 우항에 넣을 수도 있다.
fun printlnShippingLabel(person: Person){
    val address = person.company?.address
        ?: throw IllegalArgumentException("No address")
    with(address){
        println(streetAddress)
        println("$zipCode $city, $country")
    }
}

안전한 캐스트: as?

  • 자바와 마찬가지로 코틀린에서도 as로 지정한 타입으로 바꿀 수 없다면 ClassCaseException이 발생한다.
  • as를 사용할 때마다 is를 통해 변환 가능한 타입인지 검사할 수 있지만, 귀찮다.
  • as?연산자는 대상 타입으로 변환할 수 없으면 Exception발생 대신 null을 반환한다.
class Person(val firstName: String, val lastName: String){
    override fun equals(o: Any?): Boolean{
        // 타입이 일치하지 않으면 false
        val otherPerson = o as? Person ?: return false
        return otherPerson.firstName == firstName &&
                otherPerson.lastName == lastName
    }
    ...
}

널이 아님을 단언: !!

  • 널 아님 단언(not-null assertion)을 사용하면 어떤 값이든 강제로 널이 될 수 없는 타입으로 바꿀 수 있다. (느낌표 이중)
  • 실제 널에 대해 !!를 적용하면 NPE가 발생한다.
fun ignoreNulls(s: String?){
    val sNotNull = s!!
    println(sNotNull.length)
}
ignoreNulls(null)
// kotlin.KotlinNullPointerException
  • !!를 사용하여 예외가 발생했을 경우, 스택트레이스가 !!를 선언한 파일의 줄수로 나오니 한줄에 !!단언문을 쓰는 일은 피해야 한다.
person.company!!.address!!.country // x

let 함수

  • let 함수는 널이 될 수 있는 식을 더 쉽게 다를 수 있게 해준다.
  • let 함수는 자신의 수신 객체를 인자로 전달받은 람다에게 넘긴다.
    • 널이 될 수 있는 값에 대해 안전한 호출 구문을 사용해 let을 호출하되 널이 될 수 없는 타입을 받는 람다를 let에 전달한다.
fun sendEmailTo(email: String){
    println("send to $email")
}

// let 미사용
val person: Person? = getTheBestPersonInTheWorld()
if(person != null) {
    sendEmailTo(person.email)
}
// let 사용
getTheBestPersonInTheWorld()?.let { sendEmailTo(it) }

나중에 초기화할 프로퍼티 지정하기

  • 코틀린에서는 클래스 안에 널이 될 수 없는 프로퍼티를 생성자 안에서 초기화하지 않으면 다른 메서드에서 초기화가 불가능하다.
  • 초기화 값을 제공할 수 없으면, 널이 될 수 있는 타입을 사용해야한다.
    • 하지만 이렇게 되면 해당 프로퍼티를 접근할때 마다 널검사를 해야하는 번거로움이 있다.
  • junit 예제 클래스를 살펴보자.
class MyService {
    fun performAction(): String = "foo"
}
class MyTest{
    // null로 초기화 하기 위해 널이 될 수 있는 타입 선언
    private var myService: MyService? = null


    // setUp 메소드 안에서 초깃값을 지정
    @Before 
    fun setUp(){
        myService = MyService()
    }

    // 널 가능성에 신경을 써야함
    @Test fun testAction(){
        Assert.assertEqauls("foo", myService!!.performAction())
    }
}
  • 코틀린은 이런 문제를 해결하기 위해 lateinit변경자를 지원한다.
  • lateinit 변경자를 붙이면 프로퍼티를 나중에 초기화 할 수 잇다.
class MyService {
    fun performAction(): String = "foo"
}
class MyTest{
    // 초기화하지 않고 널이 될 수 없는 프로퍼티 선언
    private lateinit var myService: MyService = null

    // setUp 메소드 안에서 초깃값을 지정
    @Before 
    fun setUp(){
        myService = MyService()
    }

    // null을 신경쓸 필요가 없음
    @Test fun testAction(){
        Assert.assertEqauls("foo", myService.performAction())
    }
}
  • lateinit 프로퍼티는 항상 var여야 한다.

타입 파라미터의 널 가능성

  • 코틀린에서는 함수/클래스의 모든 타입 파라미터는 기본적으로 널이 될 수 있다.
  • 타입 파라미터 T는 최상위 클래스인 Any?의 하위 클래스이므로, 물음표가 없더라도 T는 널이될 수 있는 타입이다.
  • 따라서 널이 불가능한 타입일 때는 상한(upper bound)를 지정하여, null이 아님을 확실하게 한다.
fun<T> printHashCode(t: T){
    // t가 null이 될 수 있으므로, 안전한 호출을 써야한다.
    println(t?.hashCode())
}
fun<T: Any> printHashCode2(t: T){
    // 상한(upper bound)를 지정하면, null이 아님을 확실히한다.
    println(t.hashCode())
}

fun main(){
    printHashCode(null)
    printHashCode2(null) // 컴파일 에러
    printHashCode2(42)
}
728x90
반응형