Delegated Properties (위임 프로퍼티)
일반적으로 한 번 구현하고 라이브러리에 넣는 것이 매우 좋을 수 있는 일반적인 종류의 프로퍼티가 있다. 예제에는 다음이 포함됩니다.
- lazy 프로퍼티 : 최초 접근시 값이 계산
- observable 프로퍼티 : 프로퍼티 변경시 리스너로 알림
- 별도의 필드가 아닌 맵에 속성을 저장
위의 경우를 처리하기위해서 Kotlin은 위임 프로퍼티(Delegated Properties)를 지원
class Example {
var p: String by Delegate()
}
구문은 val/var <property name>: <Type> by <expression>
이다. by 뒤에 식이 대리자 (delegate) 이며, 프로퍼티에 대한 get()
/ set()
을 대리자 getValue()
/ setValue()
메소드에 위임한다. 프로퍼티 대리자는 인터페이스를 구현할 필요는 없지만 getValue()
메소드 (및 var에 대해 setValue()
)를 제공해야함.
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 에서 값을 읽으면 Delegate
의 getValue()
함수를 실행한다. 이 메서드의 첫 번째 파라미터는 p
를 포함한 객체이고 두 번째 파라미터는 p
자체에 대한 설명을 포함한다 (예를 들어, 프로퍼티의 이름을 구할 수 있다). 다음은 예이다.
val e = Example()
println(e.p) // Example@33a17727, thank you for delegating ‘p’ to me!
비슷하게 p
에 대입하면 setValue()
함수가 호출되고, 처음 두개의 파라매터와 동일하고 3번째 파라매터는 할당한 값을 가진다.
e.p = "NEW" // NEW has been assigned to ‘p’ in Example@33a17727.
위임된 객체의 조건은 아래에서 확인 가능하다.
Kotlin 1.1에서는 함수 또는 코드 블록 내에 위임된 프로퍼티를 선언 가능하므로 반드시 클래스의 멤버일 필요는 없습니다. 아래에서 예제를 찾을 수 있습니다. 아래가 그 사례이다.
표준 위임 (Standard Delegates)
Kotlin은 유용한 위임 팩토리 메소드를 제공한다
Lazy
lazy()
는 람다를 받아 Lazy<T>
의 인스턴스를 반환하는 함수. lazy()
에 전달된 람다를 실행하고 그 결과를 기억한다. 이후에 get()
을 호출하면 단순히 기억된 결과가 반환된다.
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main(args: Array<String>) {
println(lazyValue)
println(lazyValue)
}
computed!
Hello
Hello
lazy 프로퍼티 연산은 기본적으로 동기화된다. 값은 하나의 스레드에서만 계산되며 모든 스레드는 동일한 값을 볼 수 있다. 초기화 대리자의 동기화가 필요없다면 LazyThreadSafetyMode.PUBLICATION
을 매개 변수로 lazy()
함수에 전달해 여러 스레드가 동시에 실행할 수 있도록한다 . 항상 단일 스레드에서만 초기화하는 것을 보장하려면 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(args: Array<String>) {
val user = User()
user.name = "first"
user.name = "second"
}
<no name> -> first
first -> second
할당을 가로채서 "거부"하려면 observable()
대신 vetoable()
을 사용한다. 새 속성 값의 할당이 수행되기 전에 vetoable
에 전달된 핸들러가 호출된다.
맵에 프로퍼티 저장
위임 프로퍼티의 위임자로 맵 인스턴스를 사용 수 있다.
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
))
위임된 프로퍼티는 이 맵으로부터 값을 읽는다 (문자열 키 - 프로퍼티의 이름)
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
}
로컬 위임 프로퍼티(Local Delegated Properties) (1.1 이후)
로컬 변수를 위임 된 속성으로 선언 할 수 있다. 예를 들어 로컬 변수를 lazy 하게 만들 수 있다.
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
memoizedFoo
변수는 첫 번째 접근에서만 계산된다. someCondition
이 실패하면 전혀 계산되지 않는다.
프로퍼티 대리자 조건 (Property Delegate Requirements)
여기에서는 객체를 위임하기 위한 요구 사항을 요약한다.
읽기 전용 프로퍼티 (예 : val)의 경우 대리자는 getValue
라는 함수를 제공해야하며 다음 매개 변수를 사용한다.
thisRef
- 프로퍼티 소유자와 같거나 상위 타입이어야한다 (확장 프로퍼티 - 확장되는 타입)property
- 타입은KProperty<*>
또는 그 슈퍼 타입이어야 한다
이 함수는 프로퍼티 (또는 그 서브 타입)과 동일한 타입을 반환해야한다.
변경 가능한 프로퍼티(var)의 경우 대리자는 다음 매개 변수를 사용하는 setValue
라는 함수를 추가로 제공해야한다.
thisRef
-getValue()
와 동일property
-getValue()
와 동일- 새로운 값 - 프로퍼티 또는 그 슈퍼 타입과 같은 타입이어야 한다
getValue()
또는 setValue()
함수는 위임 클래스의 멤버 함수 또는 확장 함수로 제공될 수 있다. 후자는 원래 이러한 기능을 제공하지 않는 객체에 속성을 위임해야 할 때 편리하다. 두 기능 모두 operator
키워드로 표시해야다.
위양 클래스는 필요한 operator
가 포함된 ReadOnlyProperty
및 ReadWriteProperty
인터페이스 중 하나를 구현할 수 있다. 이러한 인터페이스는 Kotlin 표준 라이브러리에 선언되어 있다.
interface ReadOnlyProperty<in R, out T> {
operator fun getValue(thisRef: R, property: KProperty<*>): T
}
interface ReadWriteProperty<in R, T> {
operator fun getValue(thisRef: R, property: KProperty<*>): T
operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
변환 규칙 (Translation Rules)
모든 위임 된 프로퍼티에 대한 후드 아래에서 Kotlin 컴파일러는 보조 프로퍼티를 생성하고 이 프로퍼티에 위임한다. 예를 들어 프로퍼티 prop
의 경우 숨겨진 프로퍼티 prop$delegate
가 생성되고 접근자의 코드는 추가 프로퍼티에 단순히 위임한다.
class C {
var prop: Type by MyDelegate()
}
// 이 코드는 컴파일러가 생성한다
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::thisProp
는 자체를 설명하는 KProperty
타입의 반사 객체이다.
코드에서 직접 바운드 호출 가능 참조를 참조하는 this::prop
구문은 Kotlin 1.1 이후에만 사용할 수 있다.
delegate 제공 (Providing a delegate) (since 1.1)
provideDelegate
연산자를 정의하면 프로퍼티 구현이 위 된 객체를 만드는 로직을 확장할 수 있다. by
의 오른쪽에 사용되는 객체가 provideDelegate
를 멤버 또는 확장 함수로 정의하면 이 함수를 호출하여 프로퍼티 대리자 인스턴스를 만든다.
provideDelegate
의 가능한 사용 사례 중 하나는 getter 또는 setter뿐만 아니라 프로퍼티가 만들어 질 때 프로퍼티 일관성을 검사하는 것이다.
예를 들어 바인딩하기 전에 프로퍼티 이름을 확인하려면 다음과 같이 작성할 수 있다.
class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
// create delegate
}
private fun checkProperty(thisRef: MyUI, name: String) { ... }
}
fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }
class MyUI {
val image by bindResource(ResourceID.image_id)
val text by bindResource(ResourceID.text_id)
}
provideDelegate
의 매개 변수는 getValue
와 동일하다.
thisRef
- 프로퍼티 소유자와 같거나 상위 타입이어야한다 (확장 프로퍼티 - 확장된 타입)property
-KProperty<*>
또는 그 슈퍼 타입이어야한다
provideDelegate
메소드는 MyUI
인스턴스를 생성하는 동안 각 프롶터에 대해 호출되며, 필요한 유효성 검사를 즉시 수행한다.
프로퍼티와 대리자 간의 바인딩을 가로채는 이 기능이 없으면 동일한 기능을 얻기 위해 명시적으로 프로퍼티 이름을 전달해야하지만 편리하지는 않다.
// provideDelegate 기능없이 속성 이름 확인하기
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:MyDelegate()
에 대해 생성된 코드를 위의 생성된 코드와 비교한다 ( provideDelegate
메서드가 없는 경우).
class C {
var prop: Type by MyDelegate()
}
// 이 코드는 컴파일러에 의해 생성됩니다.
// provideDelegate 함수를 사용할 수있는 경우:
class C {
// "provideDelegate"를 호출하여 추가 "대리인" 프로퍼티 만든
private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
val prop: Type
get() = prop$delegate.getValue(this, this::prop)
}
provideDelegate
메소드는 보조 프로퍼티 생성에만 영향을 주며 getter 또는 setter에 대해 생성된 코드에는 영향을 미치지 않는다.