본문 바로가기

Kotlin

Kotlin - 인라인 값 클래스 (inline value classes)

https://kotlinlang.org/docs/inline-classes.html

 

Inline value classes | Kotlin

 

kotlinlang.org

 

 종종 어떤 값을 도메인 특화된 타입의 클래스로 감싸는 것이 효율 적인 경우들이 있다(wrapper class 처럼). 하지만 힙 메모리의 오버헤드가 발생 하고, 감싸는 값이 원시타입(primitive) 인 경우에는 특별히 더 비효율적이다. 왜냐하면 primitive 타입은 런타임 시에 강하게 최적화 되지만 래퍼 클래스는 그렇지 못하기 때문이다.

 이러한 문제를 해결하기 위해서 코틀린은 inline class 라는 특별한 클래스 기능을 제공한다. inline 클래스는 값 기반 클래스(value-based classes)이고, 정체성 없이 값의 성격을 갖는다.

inline class 를 선언하기 위해서는 "value" 수정자를 "class" 앞에 붙여주면 된다.

value class Password(private val s: String)

JVM을 사용하는 경우 inline 클래스 선언 앞에 @JvmInline 어노테이션을 사용해야 한다.

// For JVM backends
@JvmInline
value class Password(private val s: String)

inline 클래스의 주 생성자(primary constructor)는 반드시 단일 속성을 갖고, 런타임 시 이 단일 속성을 통해 표현된다.

 

Members

inline 클래스는 일반 클래스와 동일한 몇가지 기능들을 제공한다.

속성과 함수를 선언할수 있고, init 블록과 보조 생성자(secondary constructor)를 가질 수 있다.

inline 클래스의 속성은 backing field를 가질 수 없고, 단순한 속성만 가질 수 있다. (lateinit / delegate 속성도 불가 하다.)

@JvmInline
value class PersonInline(private val fullName: String) {
    init {
        require(fullName.isNotEmpty()) {
            "Full name sholdn't be empty"
        }
    }

    constructor(firstName: String, lastName: String): this("$firstName $lastName") {
        require(lastName.isNotBlank()) {
          "Last name shouldn't be empty"
        }
    }

    val length: Int
        get() = fullName.length

    fun greet() {
        println("Hello, $fullName")
    }
}



fun main() {
    val name1 = PersonInline("Kotlin", "Mascot")
    val name2 = PersonInline("Kodee")

    name1.greet()
    // Hello, Kotlin Mascot

    println(name2.length)
    // 5
}

 

상속

inline 클래스는 다른 클래스를 상속할수 없고 항상 final 상태이기때문에 다른 클래스가 상속 받을 수도 없다. 하지만 인터페이스는 상속 받을 수 있다.

interface Printable {
    fun prettyPrint(): String
}

@JvmInline
value class Name(val s: String) : Printable {
    override fun prettyPrint(): String = "Let's $s!"
}

fun main() {
    val name = Name("Kotlin")
    println(name.prettyPrint())
}

 

표현 (Representation)

코틀린 컴파일러는 코드 생성 시 inline 클래스의 래퍼클래스를 유지하고, Int 타입이 int 나 Integer 로 표현되는 것 과 같이 런타임시에 기본 타입을 사용할지 래퍼클래스를 사용할지 결정이 된다. 

inline 클래스는 기본타입이나 래퍼타입으로 표현이 가능하기 때문에 참조 동일성이 의미가 없고 그렇기 때문에 금지된다.

inline 클래스는 기본 타입을 지정하는 대신 제네릭 타입 변수를 사용할 수 있고, 이 경우 컴파일러는 Any? 타입이나 해당 파라미터의 상위 타입으로 변환 한다.

@JvmInline
value class UserId<T>(val value: T)

fun compute(s: UserId<String>) {} // compiler generates fun compute-<hashcode>(s: Any?)

inline 클래스는 기본 타입으로 컴파일 되기 때문에 예상하지 못한 플랫폼 시그니처 충돌과 같은 여러가지 에러를 유발 할 수 있다.

@JvmInline
value class UInt(val x: Int)

// 아래 두 함수가 동일한 형태로 표현된다.
// Represented as 'public final void compute(int x)' on the JVM
fun compute(x: Int) { }

// Also represented as 'public final void compute(int x)' on the JVM!
fun compute(x: UInt) { }

컴파일러는 위와 같은 문제를 방지하기 위해 함수명 뒤에 해시코드를 붙이는 것으로 충돌 문제를 해결한다.

fun compute(x: UInt) { }
// 컴파일 시 해시코드를 붙임
// public final void compute-<hashcode>(int x)

또는 작성자가 직접 @JvmName 어노테이션을 이용해 별도의 함수명을 지정 함 으로서 해시코드가 붙는 것을 방지 할 수 있다.

@JvmInline
value class UInt(val x: Int)

fun compute(x: Int) { }

@JvmName("computeUInt")
fun compute(x: UInt) { }

 

Inline classes vs type aliases

inline 클래스는 type alias와 매우 비슷해 보인다. 실제로 둘 모두 기본 타입을 표현하는 새로운 타입의 표현이다. 하지만, 주요한 차이점으로 type alias 는 기본 타입과 할당-호환이 가능한 반면 inline 클래스는 그렇지 못하다. inline 클래스는 실제로 새로운 타입으고 type alias는 원래 존재하는 타입의 새로운 별칭 이기 때문이다.

typealias NameTypeAlias = String

@JvmInline
value class NameInlineClass(val s: String)

fun acceptString(s: String) = println("acceptString : ${s}")
fun acceptNameTypeAlias(s: NameTypeAlias) = println("acceptNameTypeAlias : ${s}")
fun acceptNameInlineClass(s: NameInlineClass) = println("acceptNameInlineClass : ${s.s}")

fun main() {
    val nameAlias: NameTypeAlias = "a"
    val nameInlineClass: NameInlineClass = NameInlineClass("b")
    val string: String = "c"

    acceptString(string)
    acceptString(nameAlias)
//    acceptString(nameInlineClass) // compile error - 호환 불가

    acceptNameTypeAlias(string)
    acceptNameTypeAlias(nameAlias)
//    acceptNameTypeAlias(nameInlineClass)  // compile error  - 호환 불가

//    acceptNameInlineClass(string)  // compile error  - 호환 불가
//    acceptNameInlineClass(nameAlias)  // compile error  - 호환 불가
    acceptNameInlineClass(nameInlineClass)
}

 

Inline 클래스와 위임

inline 값의 위임을 통한 인터페이스의 구현이 가능하다.

interface MyInterface {
    fun bar()
    fun foo() = "foo"
}

@JvmInline
value class MyInterfaceWrapper(val myInterface: MyInterface) : MyInterface by myInterface

fun main() {
    val my = MyInterfaceWrapper(object : MyInterface {
        override fun bar() {
            // body
        }
    })
    println(my.foo()) // prints "foo"
}