Post

(Kotlin) 타입 시스템 (Any, Unit, Nothing)

코틀린에서는 원시 타입과 참조 타입(래퍼 타입, 포인터 변수)을 별도 타입으로 따로 구분하지 않는다.

  • 예를 들어 java의 int와 Integer 같이 구분하지 않고, Int 하나로 쓴다.
  • 널이 될 수 없는 타입 은 컴파일 시 알아서 원시 타입으로 표현할 수 있는건 원시 타입으로 표현해주고, 메소드를 호출하는 등 래퍼 타입이어야 하는 경우 래퍼 타입으로 변환해준다.
  • 널이 될 수 있는 타입 의 경우 null은 원시 타입에는 들어갈 수 없고, 참조 타입에만 들어갈 수 있으므로 무조건 래퍼 타입으로 컴파일된다.

타입 상한 : 제네릭의 타입 파라미터

T 는 T?가 아니어도 널이 될 수 있는 타입이다.

  • 제네릭 클래스(T)의 경우 T에 원시 타입을 지정하더라도 내부적으로는 항상 그에 대한 박스 타입을 사용한다. JVM이 타입 인자로 원시 타입을 허용하지 않기 때문.
  • 타입 파라미터가 널이 아님을 확실히 해주기 위해서는 반드시 타입 상한(upper bound)을 정해주어야 한다.
1
2
3
4
5
6
fun <T> printHashCode(t: T) {    // T는 Any?로 추론된다.
println(t?.hashCode())    // 따라서 ?. 를 사용해야 한다.
}
>>> printHashCode(null)    // 에러 안남
null

1
2
3
4
5
6
7
fun <T: Any> printHashCode(t: T) {    // T는 Any로 추론된다.
println(t.hashCode())
}
>>> printHashCode(null)  // 에러남
error: type parameter bound for T in fun <T : Any> printHashCode(t: T): Unit
is not satisfied: inferred type Nothing? is not a subtype of Any

비교 시에는

묵시적 형변환 해주지 않는다. 묵시적 형변환 해주지 않기 때문에 주의해야 한다. 특히 IntLong을 비교할 때.

1
2
3
4
5
>>> val x = 1
>>> val list = listOf(1L, 2L)
>>> x in list
error: type inference failed.

연산자는 묵시적 형변환을 지원하도록 오버로딩 되어 있다.

1
2
3
val i: Int = 1024
val l: Long = i + 1024L

원시 타입 리터럴

1
2
3
4
5
6
Long   : 10000L
Double : 0.12    1.2e-5
Float  : 12.4F
0xDEADBEEF
0b101110

Any, Any? : 최상위 타입

  • 자바에서는 참조 타입만 Object를 정점으로 하는 타입 계층에 포함되며, 원시 타입은 계층에 속해있지 않다.
  • 코틀린에서는 Any가 원시 타입을 포함한 모든 타입의 조상이다. 그래서 원시 타입을 Any에 담을 수 있으며 담게되면 Any는 참조 타입이므로 박싱된다.
  • Anyjava.lang.Object에 대응하기는 하지만, toString(), equals(), hashCode()를 제외한 다른 메소드(wait(), notify()등)은 kt Any에서 사용할 수 없다. 사용하려면 java.lang.Object로 캐스트해야 한다.

Unit 타입 : void

반환 타입 없이 선언한 block body 함수는 자동으로 리턴 타입이 Unit이다. Unitvoid와 달리 타입이다. 따라서 타입 파라미터T로 쓸 수 있다. Unit타입에 속하는 값은 딱 하나 있으며, 그 이름도 kt Unit이다. 리턴 타입이 Unit인 함수는 묵시적으로 Unit을 반환한다. 즉, 반환하는 값이 없는게 아니다. 이 것이 Nothing과의 차이다. * 함수형 프로그래밍에서 Unit은 ‘단 하나의 인스턴스만 갖는 타입’을 의미한다.

Nothing 타입 : 엘비스 연산자의 우항에 들어가는 함수의 리턴 타입

1
2
3
4
5
6
7
8
9
10
11
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
>>> val company = Company("n", Address("seoul", "kor"))
>>> val address = company.address
>>> println(address.city)
Error: Only safe (?.) or non-null asserted (!!.) calls...
>>> val address = company.address ?: fail("No address")
>>> println(address.city)
seoul

  • ?.가 아니라 그냥 .으로 사용할 수 있는 것은 엘비스 연산자 덕분이다.

    • Nothing 타입은 “이 함수는 항상 실패하는 함수”라는 정보를 컴파일러에게 알려주기는 하지만, ?: 뒤에 다른 것을 적어도 그냥 .으로 연결 가능하다.
  • 그럼에도 Nothing 타입을 사용해야 하는 이유는,

    • 타입을 생략해서 함수의 리턴 타입이 Unit이 되면, 다른 아무거나 타입을 리턴 타입으로 지정하는 경우 엘비스 연산자의 우항이 절대 실행되지 않더라도 스마트 캐스트가 동작해 address의 타입이 Any가 되어 버린다.
    • 따라서 별도의 캐스팅이 필요하거나, null 아님을 다시 체크해줘야 한다.
1
2
3
4
5
6
7
8
9
fun fail(message: String):**Unit** {
throw IllegalStateException(message)
}
>>> val address = company.address ?: fail("No address")
>>> val i : Int = address
Error:(13, 19) Kotlin: Type mismatch: inferred type is Any but Int was expected
>>> println(address.city)
Error:(14, 21) Kotlin: Unresolved reference: city

그래서 엘비스 연산자를 사용할 때 우항에 적는 함수의 리턴 타입은 좌항과 동일(이 경우 Address)하거나, Nothing이어야만 한다

This post is licensed under CC BY 4.0 by the author.