본문으로 건너뛰기

위임된 속성

몇 가지 일반적인 종류의 프로퍼티의 경우, 필요할 때마다 직접 구현할 수도 있지만, 한 번 구현하여 라이브러리에 추가하고 나중에 재사용하는 것이 더 유용합니다. 예를 들면 다음과 같습니다.

  • Lazy 프로퍼티: 값은 처음 접근할 때만 계산됩니다.
  • Observable 프로퍼티: 이 프로퍼티의 변경 사항에 대해 리스너에게 알림이 전달됩니다.
  • 각 프로퍼티에 대해 별도의 필드 대신 map 에 프로퍼티를 저장합니다.

이러한 (및 기타) 경우를 처리하기 위해 코틀린은 위임된 프로퍼티(delegated properties) 를 지원합니다.

class Example {
var p: String by Delegate()
}

구문은 val/var <프로퍼티 이름>: <Type> by <expression>입니다. by 뒤의 표현식은 delegate 입니다. 프로퍼티에 해당하는 get() (및 set())이 해당 getValue()setValue() 메서드에 위임되기 때문입니다. 프로퍼티 delegate는 인터페이스를 구현할 필요는 없지만 getValue() 함수 (그리고 var의 경우 setValue())를 제공해야 합니다.

예를 들어:

import kotlin.reflect.KProperty

class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}

Delegate의 인스턴스에 위임하는 p에서 읽을 때 DelegategetValue() 함수가 호출됩니다. 첫 번째 매개변수는 p를 읽는 객체이고 두 번째 매개변수는 p 자체에 대한 설명을 담고 있습니다. (예를 들어 이름을 가져올 수 있습니다.)

val e = Example()
println(e.p)

다음이 출력됩니다.

Example@33a17727, thank you for delegating 'p' to me!

마찬가지로 p에 할당하면 setValue() 함수가 호출됩니다. 처음 두 매개변수는 동일하고 세 번째 매개변수는 할당되는 값을 담고 있습니다.

e.p = "NEW"

다음이 출력됩니다.

NEW has been assigned to 'p' in Example@33a17727.

위임된 객체에 대한 요구 사항 사양은 아래에서 확인할 수 있습니다.

함수 또는 코드 블록 내에서 위임된 프로퍼티를 선언할 수 있습니다. 클래스의 멤버일 필요는 없습니다. 아래에서 예제를 찾을 수 있습니다.

표준 delegate

Kotlin 표준 라이브러리는 여러 유용한 종류의 delegate에 대한 팩토리 메서드를 제공합니다.

Lazy 프로퍼티

lazy()는 람다를 가져와서 lazy 프로퍼티를 구현하기 위한 delegate로 사용할 수 있는 Lazy<T> 인스턴스를 반환하는 함수입니다. get()에 대한 첫 번째 호출은 lazy()에 전달된 람다를 실행하고 결과를 기억합니다. 이후의 get() 호출은 단순히 기억된 결과를 반환합니다.

val lazyValue: String by lazy {
println("computed!")
"Hello"
}

fun main() {
println(lazyValue)
println(lazyValue)
}

기본적으로 lazy 프로퍼티의 평가는 synchronized 됩니다. 값은 하나의 스레드에서만 계산되지만 모든 스레드가 동일한 값을 보게 됩니다. 초기화 delegate의 동기화가 여러 스레드가 동시에 실행하도록 허용하는 데 필요하지 않은 경우 LazyThreadSafetyMode.PUBLICATIONlazy()에 대한 매개변수로 전달합니다.

초기화가 프로퍼티를 사용하는 스레드와 항상 동일한 스레드에서 발생할 것이라고 확신하는 경우 LazyThreadSafetyMode.NONE을 사용할 수 있습니다. 스레드 안전성을 보장하지 않으며 관련 오버헤드를 발생시키지 않습니다.

Observable 프로퍼티

Delegates.observable() 두 개의 인수를 사용합니다. 초기 값과 수정에 대한 핸들러입니다.

핸들러는 프로퍼티에 할당할 때마다 호출됩니다 (할당이 수행된 후). 세 가지 매개변수가 있습니다. 할당되는 프로퍼티, 이전 값 및 새 값입니다.

import kotlin.properties.Delegates

class User {
var name: String by Delegates.observable("<no name>") {
prop, old, new `->`
println("$old `->` $new")
}
}

fun main() {
val user = User()
user.name = "first"
user.name = "second"
}

할당을 가로채서 거부하려면 observable() 대신 vetoable()을 사용합니다. vetoable에 전달된 핸들러는 새 프로퍼티 값이 할당되기 전에 호출됩니다.

다른 프로퍼티에 위임

프로퍼티는 getter와 setter를 다른 프로퍼티에 위임할 수 있습니다. 이러한 위임은 최상위 및 클래스 프로퍼티(멤버 및 확장) 모두에 사용할 수 있습니다. delegate 프로퍼티는 다음 중 하나일 수 있습니다.

  • 최상위 프로퍼티
  • 동일한 클래스의 멤버 또는 확장 프로퍼티
  • 다른 클래스의 멤버 또는 확장 프로퍼티

프로퍼티를 다른 프로퍼티에 위임하려면 delegate 이름에 :: 한정자를 사용합니다. 예를 들어 this::delegate 또는 MyClass::delegate와 같습니다.

var topLevelInt: Int = 0
class ClassWithDelegate(val anotherClassInt: Int)

class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
var delegatedToMember: Int by this::memberInt
var delegatedToTopLevel: Int by ::topLevelInt

val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt

이는 예를 들어 이전 버전과의 호환성을 유지하면서 프로퍼티 이름을 바꾸려는 경우에 유용할 수 있습니다. 새 프로퍼티를 도입하고 이전 프로퍼티에 @Deprecated 어노테이션을 추가하고 해당 구현을 위임합니다.

class MyClass {
var newName: Int = 0
@Deprecated("Use 'newName' instead", ReplaceWith("newName"))
var oldName: Int by this::newName
}
fun main() {
val myClass = MyClass()
// Notification: 'oldName: Int' is deprecated.
// Use 'newName' instead
myClass.oldName = 42
println(myClass.newName) // 42
}

맵에 프로퍼티 저장

일반적인 사용 사례 중 하나는 맵에 프로퍼티 값을 저장하는 것입니다. 이는 JSON을 파싱하거나 다른 동적 작업을 수행하는 것과 같은 애플리케이션에서 자주 발생합니다. 이 경우 맵 인스턴스 자체를 위임된 프로퍼티의 delegate로 사용할 수 있습니다.

class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}

이 예제에서 생성자는 맵을 사용합니다.

val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))

위임된 프로퍼티는 프로퍼티 이름과 연결된 문자열 키를 통해 이 맵에서 값을 가져옵니다.

class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}

fun main() {
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))

println(user.name) // Prints "John Doe"
println(user.age) // Prints 25

}

읽기 전용 Map 대신 MutableMap을 사용하는 경우 var의 프로퍼티에도 작동합니다.

class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}

로컬 위임된 프로퍼티

로컬 변수를 위임된 프로퍼티로 선언할 수 있습니다. 예를 들어 로컬 변수를 lazy로 만들 수 있습니다.

fun example(computeFoo: () `->` Foo) {
val memoizedFoo by lazy(computeFoo)

if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}

memoizedFoo 변수는 처음 접근할 때만 계산됩니다. someCondition이 실패하면 변수는 전혀 계산되지 않습니다.

프로퍼티 delegate 요구 사항

읽기 전용 프로퍼티(val)의 경우 delegate는 다음 매개변수를 사용하여 연산자 함수 getValue()를 제공해야 합니다.

  • thisRef프로퍼티 소유자와 동일한 유형이거나 상위 유형이어야 합니다 (확장 프로퍼티의 경우 확장되는 유형이어야 함).
  • propertyKProperty<*> 유형 또는 해당 상위 유형이어야 합니다.

getValue()는 프로퍼티와 동일한 유형(또는 하위 유형)을 반환해야 합니다.

class Resource

class Owner {
val valResource: Resource by ResourceDelegate()
}

class ResourceDelegate {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return Resource()
}
}

변경 가능한 프로퍼티(var)의 경우 delegate는 다음 매개변수를 사용하여 연산자 함수 setValue()를 추가로 제공해야 합니다.

  • thisRef프로퍼티 소유자와 동일한 유형이거나 상위 유형이어야 합니다 (확장 프로퍼티의 경우 확장되는 유형이어야 함).
  • propertyKProperty<*> 유형 또는 해당 상위 유형이어야 합니다.
  • value는 프로퍼티와 동일한 유형(또는 상위 유형)이어야 합니다.
class Resource

class Owner {
var varResource: Resource by ResourceDelegate()
}

class ResourceDelegate(private var resource: Resource = Resource()) {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return resource
}
operator fun setValue(thisRef: Owner, property: KProperty<*>, value: Any?) {
if (value is Resource) {
resource = value
}
}
}

getValue() 및/또는 setValue() 함수는 delegate 클래스의 멤버 함수 또는 확장 함수로 제공될 수 있습니다. 후자는 원래 이러한 함수를 제공하지 않는 객체에 프로퍼티를 위임해야 할 때 유용합니다. 두 함수 모두 operator 키워드로 표시되어야 합니다.

Kotlin 표준 라이브러리의 ReadOnlyPropertyReadWriteProperty 인터페이스를 사용하여 새 클래스를 만들지 않고도 익명 객체로 delegate를 만들 수 있습니다. 이러한 인터페이스는 필요한 메서드를 제공합니다. getValue()ReadOnlyProperty에 선언됩니다. ReadWriteProperty는 이를 확장하고 setValue()를 추가합니다. 즉, ReadOnlyProperty가 예상될 때마다 ReadWriteProperty를 전달할 수 있습니다.

fun resourceDelegate(resource: Resource = Resource()): ReadWriteProperty<Any?, Resource> =
object : ReadWriteProperty<Any?, Resource> {
var curValue = resource
override fun getValue(thisRef: Any?, property: KProperty<*>): Resource = curValue
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Resource) {
curValue = value
}
}

val readOnlyResource: Resource by resourceDelegate() // ReadWriteProperty as val
var readWriteResource: Resource by resourceDelegate()

위임된 프로퍼티에 대한 변환 규칙

내부적으로 Kotlin 컴파일러는 일부 종류의 위임된 프로퍼티에 대해 보조 프로퍼티를 생성한 다음 해당 프로퍼티에 위임합니다.

노트

최적화 목적으로 컴파일러는 몇 가지 경우에 보조 프로퍼티를 생성하지 않습니다. 다른 프로퍼티에 위임하는 경우의 예에서 최적화에 대해 알아보세요.

예를 들어 프로퍼티 prop의 경우 숨겨진 프로퍼티 prop$delegate를 생성하고 접근자 코드는 이 추가 프로퍼티에 위임합니다.

class C {
var prop: Type by MyDelegate()
}

// this code is generated by the compiler instead:
class C {
private val prop$delegate = MyDelegate()
var prop: Type
get() = prop$delegate.getValue(this, this::prop)
set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

Kotlin 컴파일러는 인수에 prop에 대한 필요한 모든 정보를 제공합니다. 첫 번째 인수 this는 외부 클래스 C의 인스턴스를 참조하고 this::propprop 자체를 설명하는 KProperty 유형의 리플렉션 객체입니다.

위임된 프로퍼티에 대한 최적화된 사례

delegate가 다음인 경우 $delegate 필드가 생략됩니다.

  • 참조된 프로퍼티:

    class C<Type> {
    private var impl: Type = ...
    var prop: Type by ::impl
    }
  • 명명된 객체:

    object NamedObject {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ...
    }

    val s: String by NamedObject
  • 동일한 모듈에서 백업 필드와 기본 getter가 있는 final val 프로퍼티:

    val impl: ReadOnlyProperty<Any?, String> = ...

    class A {
    val s: String by impl
    }
  • 상수 표현식, enum 항목, this, null. this의 예:

    class A {
    operator fun getValue(thisRef: Any?, property: KProperty<*>) ...

    val s by this
    }

다른 프로퍼티에 위임할 때의 변환 규칙

다른 프로퍼티에 위임할 때 Kotlin 컴파일러는 참조된 프로퍼티에 대한 즉각적인 접근을 생성합니다. 이는 컴파일러가 필드 prop$delegate를 생성하지 않음을 의미합니다. 이 최적화는 메모리 절약에 도움이 됩니다.

예를 들어 다음 코드를 살펴보겠습니다.

class C<Type> {
private var impl: Type = ...
var prop: Type by ::impl
}

prop 변수의 프로퍼티 접근자는 위임된 프로퍼티의 getValuesetValue 연산자를 건너뛰고 impl 변수를 직접 호출합니다. 따라서 KProperty 참조 객체가 필요하지 않습니다.

위의 코드의 경우 컴파일러는 다음 코드를 생성합니다.

class C<Type> {
private var impl: Type = ...

var prop: Type
get() = impl
set(value) {
impl = value
}

fun getProp$delegate(): Type = impl // This method is needed only for reflection
}

delegate 제공

provideDelegate 연산자를 정의하면 프로퍼티 구현이 위임되는 객체 생성을 위한 논리를 확장할 수 있습니다. by의 오른쪽에 사용된 객체가 provideDelegate를 멤버 또는 확장 함수로 정의하는 경우 해당 함수가 호출되어 프로퍼티 delegate 인스턴스를 만듭니다.

provideDelegate의 가능한 사용 사례 중 하나는 초기화 시 프로퍼티의 일관성을 확인하는 것입니다.

예를 들어 바인딩하기 전에 프로퍼티 이름을 확인하려면 다음과 같이 작성할 수 있습니다.

class ResourceDelegate<T> : ReadOnlyProperty<MyUI, T> {
override fun getValue(thisRef: MyUI, property: KProperty<*>): T { ... }
}

class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
// create delegate
return ResourceDelegate()
}

private fun checkProperty(thisRef: MyUI, name: String) { ... }
}

class MyUI {
fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }

val image by bindResource(ResourceID.image_id)
val text by bindResource(ResourceID.text_id)
}

provideDelegate의 매개변수는 getValue의 매개변수와 동일합니다.

  • thisRef는 _프로퍼티 소유자_와 동일한 유형이거나 상위 유형이어야 합니다(확장 프로퍼티의 경우 확장되는 유형이어야 함).
  • propertyKProperty<*> 유형 또는 해당 상위 유형이어야 합니다.

provideDelegate 메서드는 MyUI 인스턴스를 만드는 동안 각 프로퍼티에 대해 호출되며 필요한 유효성 검사를 즉시 수행합니다.

프로퍼티와 delegate 간의 바인딩을 가로채는 기능이 없으면 동일한 기능을 달성하기 위해 프로퍼티 이름을 명시적으로 전달해야 합니다. 이는 그다지 편리하지 않습니다.

// Checking the property name without "provideDelegate" functionality
class MyUI {
val image by bindResource(ResourceID.image_id, "image")
val text by bindResource(ResourceID.text_id, "text")
}

fun <T> MyUI.bindResource(
id: ResourceID<T>,
propertyName: String
): ReadOnlyProperty<MyUI, T> {
checkProperty(this, propertyName)
// create delegate
}

생성된 코드에서 provideDelegate 메서드는 보조 prop$delegate 프로퍼티를 초기화하는 데 사용됩니다. val prop: Type by MyDelegate() 프로퍼티 선언에 대해 생성된 코드를 provideDelegate 메서드가 없는 경우 에서 생성된 코드와 비교합니다.

class C {
var prop: Type by MyDelegate()
}

// this code is generated by the compiler
// when the 'provideDelegate' function is available:
class C {
// calling "provideDelegate" to create the additional "delegate" property
private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
var prop: Type
get() = prop$delegate.getValue(this, this::prop)
set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

provideDelegate 메서드는 보조 프로퍼티 생성에만 영향을 미치고 getter 또는 setter에 대해 생성된 코드에는 영향을 미치지 않습니다.

표준 라이브러리의 PropertyDelegateProvider 인터페이스를 사용하면 새 클래스를 만들지 않고도 delegate 제공자를 만들 수 있습니다.

val provider = PropertyDelegateProvider { thisRef: Any?, property `->`
ReadOnlyProperty<Any?, Int> {_, property `->` 42 }
}
val delegate: Int by provider