•
Kotlin에서 성능 최적화를 위해 도입된 특별한 종류의 클래스이다.
•
때로는 특정 도메인에 맞는 타입을 만들기 위해 값을 클래스로 감싸는 것이 유용할 수 있다.
•
하지만 이런 방식은 런타임에서 추가적인 힙 할당을 유발하여 성능에 부담을 줄 수 있다.
•
특히 감싼 값이 원시 타입일 경우 런타임에선 최적화가 잘 되지만 클래스로 감싸게 되면 최적화를 받지 못해 성능 손실이 크다.
•
이러한 문제를 해결하기 위해 inline 클래스라는 특별한 클래스 종류를 제공하고 value-based class의 하위 집합으로 identity가 없고 오직 값만 가진다.
인라인 클래스
•
inline 클래스를 선언하기 위해서는 클래스 이름 앞에 value 키워드를 사용한다.
value class Password(private val s: String)
Kotlin
복사
•
JVM에 대한 inline 클래스를 선언하려면 클래스 선언 앞에 value 키워드와 함께 @JvmInline 어노테이션을 추가한다.
// For JVM backends
@JvmInline
value class Password(private val s: String)
Kotlin
복사
•
inline 클래스는 기본 생성자에서 초기화된 단일 프로퍼티를 가져야 한다.
•
런타임에서는 이 단일 프로퍼티를 사용하여 inline 클래스의 인스턴스가 표현된다.
// 클래스 'Password'의 실제 인스턴스화는 일어나지 않음
// 런타임에서 'securePassword'는 단지 'String' 값을 가짐
val securePassword = Password("Don't try this in production")
Kotlin
복사
•
Password 클래스의 실제 인스턴스화는 일어나지 않으며 런타임에서 securePassword는 단지 String 값만을 포함한다.
•
inline 함수의 내용이 호출되는 곳에 inline되는 것과 마찬가지로 inline 클래스의 데이터도 사용되는 곳에 inline으로 포함된다.
인라인 클래스의 멤버
•
inline 클래스는 일반 클래스의 일부 기능을 지원한다.
•
프로퍼티와 함수를 선언하고 init 블록과 보조 생성자를 가질 수 있다.
•
inline 클래스 프로퍼티는 백킹 필드를 가질 수 없다.
•
단순 계산 가능한 프로퍼티만 가질 수 있으며 lateinit 이나 위임 프로퍼티 같은 기능을 사용할 수 없다.
@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() // greet() 함수가 정적 메서드처럼 호출됨
println(name2.length) // 프로퍼티의 getter가 정적 메서드처럼 호출됨
}
Kotlin
복사
인라인 클래스의 상속
•
inline 클래스는 인터페이스를 상속 받을 수 있다.
•
inline 클래스는 다른 클래스를 상속할 수 없으며 항상 final로 선언된다.
•
inline 클래스의 메서드와 프로퍼티는 정적 메서드처럼 호출된다.
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()) // Still called as a static method
}
Kotlin
복사
인라인 클래스의 표현
•
Kotlin 컴파일러는 inline 클래스를 위해 각 클래스에 대한 래퍼를 유지한다.
•
런타임 시에 inline 클래스의 인스턴스는 래퍼로 표현될 수도 있고 해당 클래스가 감싸고 있는 원시 타입으로도 표현될 수 있다.
•
Kotlin 컴파일러는 최적화된 성능을 내기 위해 가능하면 래퍼 대신 원시 타입을 사용하려고 한다.
•
그러나 inline 클래스가 다른 타입으로 사용될 떄 래퍼를 유지하는 경우도 있다.
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: Foo 자체로 사용됨
asGeneric(f) // boxed: 제네릭 타입 T로 사용됨
asInterface(f) // boxed: 인터페이스 I 타입으로 사용됨
asNullable(f) // boxed: Foo? 타입으로 사용됨, 이는 Foo와는 다름
// 아래에서 'f'는 먼저 'id'에 전달될 때 boxed되며, 반환될 때 unboxed됨
// 결국 'c'는 unboxed 상태(단지 '42')를 가지며, 이는 'f'와 같음
val c = id(f)
}
Kotlin
복사
•
inline 클래스는 기본 값과 래퍼 두 가지 형태로 표현될 수 있으므로 참조 동등성을 비교하는 것은 의미가 없기 때문에 참조 동등성 비교는 금지된다.
@JvmInline
value class UserId<T>(val value: T)
fun compute(s: UserId<String>) {} // compiler generates fun compute-<hashcode>(s: Any?)
Kotlin
복사
•
inline 클래스는 기본 타입으로 제네릭 타입 매개변수를 가질 수도 있다.
•
이 경우 컴파일러는 이를 Any? 또는 해당 타입 파라미터의 상위 경계로 매핑한다.
맹글링
•
inline 클래스는 기본 타입으로 컴파일되기 때문에 플랫폼 서명 충돌과 같은 문제가 발생할 수 있다.
•
예를 들어, 동일한 이름의 함수가 원시 타입과 inline 클래스를 각각 매개변수로 받을 때 컴파일 후에 동일한 JVM 서명을 가지게 되어 충돌이 발생할 수 있다.
@JvmInline
value class UInt(val x: Int)
// JVM에서 'public final void compute(int x)'로 표현됨
fun compute(x: Int) { }
// 또한 'public final void compute(int x)'로 표현됨!
fun compute(x: UInt) { }
Kotlin
복사
•
Kotlin 컴파일러는 이러한 문제를 해결하기 위해 inline 클래스를 사용하는 함수의 이름에 안정적인 해시코드를 추가하여 함수 이름을 맹글링한다.
•
따라서 fun compute(x: UInt) 는 public final void compute-<hashcode>(int x)로 변환되며 함수 이름이 고유하기 때문에 동일한 서명을 가진 다른 함수들과의 충돌을 피하게 된다.
Java 코드에서 호출
•
Java 코드에서 inline 클래스를 사용하는 Kotlin 함수를 호출하려면 맹글링된 함수 이름을 사용해야 하므로 불편함이 생길 수 있다.
•
이를 해결하기 위해 @JvmName 어노테이션을 추가하여 맹글링을 수동으로 비활성화하며 함수의 이름을 명시적으로 지정할 수 있다.
@JvmInline
value class UInt(val x: Int)
fun compute(x: Int) { }
@JvmName("computeUInt")
fun compute(x: UInt) { }
Kotlin
복사
인라인 클래스 vs 타입 별칭
•
inline 클래스와 타입 별칭은 둘 다 새로운 타입을 도입하는 것처럼 보이며 런타임 시에는 모두 기본 타입으로 표현되기 때문에 유사해 보일 수 있다.
•
그러나 타입 별칭은 새로운 이름을 부여하는 것에 불과하며 기본 타입과 호환이 가능하지만 inline 클래스는 실제로 새로운 타입을 도입하며 기본 타입과 호환되지 않는다는 차이점이 있다.
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: 별칭을 기본 타입 대신 전달 가능
acceptString(nameInlineClass) // Not OK: inline 클래스를 기본 타입 대신 전달할 수 없음
// 반대로:
acceptNameTypeAlias(string) // OK: 기본 타입을 별칭 대신 전달 가능
acceptNameInlineClass(string) // Not OK: 기본 타입을 inline 클래스 대신 전달할 수 없음
}
Kotlin
복사
인라인 클래스와 위임
•
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"
}
Kotlin
복사
•
MyInterfaceWrapper inline 클래스는 MyInterface by myInterface 를 사용하여 전달된 myInterface 객체에게 인터페이스의 메서드 구현을 위임한다.