인라인 value 클래스 (Inline value classes)
비즈니스 로직에서는 종종 어떤 유형 주위에 래퍼를 생성해야 할 필요가 있습니다. 그러나 이로 인해 추가적인 힙 할당으로 인한 런타임 오버헤드가 발생합니다. 게다가 래핑된 유형이 기본 유형인 경우 성능 저하가 심각해집니다. 왜냐하면 기본 유형은 일반적으로 런타임에 의해 강력하게 최적화되지만, 해당 래퍼는 특별한 처리를 받지 못하기 때문입니다.
이러한 문제를 해결하기 위해 Kotlin은 특별한 종류의 클래스를 소개합니다. 이 클래스는 인라인 클래스라고 불리며, 값 기반 클래스의 하위 집합입니다. 이들은 식별성이 없고 값만을 보유할 수 있습니다.
인라인 클래스를 선언하려면 클래스 이름 앞에 value 수정자를 사용하십시오:
value class Password(private val s: String)
JVM 백엔드용으로 인라인 클래스를 선언하려면 클래스 선언 앞에 value 수정자와 @JvmInline 주석을 함께 사용하십시오:
// For JVM backends
@JvmInline
value class Password(private val s: String)
인라인 클래스는 주 생성자에서 초기화되는 단일 속성을 가져야 합니다. 런타임에서는 인라인 클래스의 인스턴스가 이 단일 속성을 사용하여 나타낼 것입니다 (아래에서 런타임 표현에 대한 자세한 내용을 확인하십시오):
// 클래스 'Password'의 실제 인스턴스화가 발생하지 않음
// 런타임에서 'securePassword'는 단순히 'String'을 포함합니다
val securePassword = Password("Don't try this in production")
이것이 인라인 클래스의 주요 기능입니다. 이로 인해 클래스의 데이터가 해당 사용처로 인라인되며 (인라인 함수 내용이 호출 사이트로 인라인되는 방식과 유사), 클래스의 인스턴스가 인라인됩니다.
멤버 (Members)
인라인 클래스는 일반 클래스의 일부 기능을 지원합니다. 특히, 속성과 함수를 선언할 수 있으며, init 블록과 보조 생성자를 가질 수 있습니다.
@JvmInline
value class Person(private val fullName: String) {
init {
require(fullName.isNotEmpty()) {
"Full name shouldn'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 = Person("Kotlin", "Mascot")
val name2 = Person("Kodee")
name1.greet() // the `greet()` function is called as a static method
println(name2.length) // property getter is called as a static method
}
인라인 클래스의 속성은 백킹 필드를 가질 수 없습니다. 그들은 간단한 계산 가능한 속성만 가질 수 있으며 (lateinit/delegated 속성은 불가능합니다).
상속 (Inheritance)
인라인 클래스는 인터페이스로부터 상속 받을 수 있습니다:
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)
인라인 클래스는 클래스 계층 구조에 참여하는 것이 금지되어 있습니다. 이는 인라인 클래스가 다른 클래스를 확장할 수 없고 항상 final로 선언된다는 것을 의미합니다.
생성된 코드에서 Kotlin 컴파일러는 각 인라인 클래스에 대한 래퍼(wrapper)를 유지합니다. 인라인 클래스 인스턴스는 런타임에서 래퍼 또는 기본 타입으로 표현될 수 있습니다. 이는 Int가 원시 int 또는 래퍼 Integer로 표현될 수 있는 방식과 유사합니다.
Kotlin 컴파일러는 가장 성능이 우수하고 최적화된 코드를 생성하기 위해 래퍼 대신 기본 타입을 사용하는 것을 선호합니다. 그러나 때로는 래퍼를 유지해야 할 필요가 있을 수 있습니다. 일반적인 지침으로, 다른 타입으로 사용될 때 인라인 클래스는 상자에 넣어집니다.
interface I
@JvmInline
value class Foo(val i: Int) : I
fun asInline(f: Foo) {}
fun <T> asGeneric(x: T) {}
fun asInterface(i: I) {}
fun asNullable(i: Foo?) {}
fun <T> id(x: T): T = x
fun main() {
val f = Foo(42)
asInline(f) // unboxed: used as Foo itself
asGeneric(f) // boxed: used as generic type T
asInterface(f) // boxed: used as type I
asNullable(f) // boxed: used as Foo?, which is different from Foo
// below, 'f' first is boxed (while being passed to 'id') and then unboxed (when returned from 'id')
// In the end, 'c' contains unboxed representation (just '42'), as 'f'
val c = id(f)
}
인라인 클래스는 기본 값과 래퍼로 모두 표현될 수 있기 때문에 참조 동등성은 의미가 없으며 따라서 금지됩니다.
인라인 클래스는 또한 기본 유형으로 제네릭 유형 매개변수를 가질 수 있습니다. 이 경우 컴파일러는 해당 매개변수를 Any? 또는 일반적으로 유형 매개변수의 상한으로 매핑합니다.
@JvmInline
value class UserId<T>(val value: T)
fun compute(s: UserId<String>) {} // compiler generates fun compute-<hashcode>(s: Any?)
이름 바꾸기 (Mangling)
인라인 클래스는 기본 유형으로 컴파일되기 때문에 예상치 못한 플랫폼 시그니처 충돌과 같은 다양한 모호한 오류가 발생할 수 있습니다:
@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-<해시코드>(int x)로 표시되어 충돌 문제를 해결합니다.
자바 코드에서 호출하기 (Calling from Java code)
자바 코드에서 인라인 클래스를 받아들이는 함수를 호출할 수 있습니다. 이를 위해서는 수동으로 이름 변경을 비활성화해야 합니다: 함수 선언 앞에 @JvmName 주석을 추가하십시오:
@JvmInline
value class UInt(val x: Int)
fun compute(x: Int) { }
@JvmName("computeUInt")
fun compute(x: UInt) { }
인라인 클래스 vs 타입 별칭 (Inline classes vs type aliases)
첫눈에는 인라인 클래스와 타입 별칭이 매우 유사해 보입니다. 실제로 둘 다 새로운 타입을 도입하며 런타임에서는 기본 타입으로 표현됩니다.
하지만 결정적인 차이는 타입 별칭은 기본 타입과 할당 호환성이 있지만(같은 기본 타입을 가진 다른 타입 별칭과도 호환됨), 인라인 클래스는 호환성이 없다는 것입니다.
다시 말해, 인라인 클래스는 실제로 새로운 타입을 도입하며, 기존 타입에 대한 대안적인 이름(별칭)만 도입하는 타입 별칭과는 달라요.
typealias NameTypeAlias = String
@JvmInline
value class NameInlineClass(val s: String)
fun acceptString(s: String) {}
fun acceptNameTypeAlias(n: NameTypeAlias) {}
fun acceptNameInlineClass(p: NameInlineClass) {}
fun main() {
val nameAlias: NameTypeAlias = ""
val nameInlineClass: NameInlineClass = NameInlineClass("")
val string: String = ""
acceptString(nameAlias) // OK: pass alias instead of underlying type
acceptString(nameInlineClass) // Not OK: can't pass inline class instead of underlying type
// And vice versa:
acceptNameTypeAlias(string) // OK: pass underlying type instead of alias
acceptNameInlineClass(string) // Not OK: can't pass underlying type instead of inline class
}
인라인 클래스와 위임 (Inline classes and delegation)
인터페이스를 통한 인라인된 클래스의 인라인된 값에 대한 위임 구현이 허용됩니다.
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"
}
'Kotlin' 카테고리의 다른 글
[Kotlin] inline 사용법 (2) - reified 사용법 (0) | 2023.08.15 |
---|---|
[Kotlin] inline 사용법 (1) - 왜 inline function, class, property를 사용하는 걸까? (0) | 2023.08.13 |
[Kotlin] Kotlin 공식 문서 번역 - 인라인 함수 (Inline functions) (0) | 2023.08.12 |
[Kotlin] Kotlin 공식 문서 번역 - 고차 함수와 람다 (High-order functions and lambdas) (0) | 2023.08.12 |
[Kotlin] Extensions 사용법 총 정리 (1) - Extensions란? (0) | 2023.06.18 |
댓글