일반적으로 프로퍼티는 필요할 때마다 수동으로 구현할 수 있지만, 한 번 구현한 후 필요할 때마다 재사용하는 것이 더 유용하다.
대상은 다음과 같다.
- Lazy 속성: 최초 접근시에만 값을 계산.
- Observable 속성: 이 속성의 변경사항을 리스너로 알림.
- 별도의 필드가 아닌 맵에 속성을 저장.
이 경우들을 처리하기 위해 Kotlin은 Delegated Properties를 지원한다.
class Example{
var p: String by Delegate()
}
형태는 val/var <property name>: <Type> by <expression> 이다. by 뒤에 붙은 expression이 대리자(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)
//result -> Example@33a17727, thank you for delegating 'p' to me!
반대로 p에 대입하면 setValue() 함수가 호출되고, 처음 두 개의 파라미터는 위와 동일하고 3번째 파라미터는 할당받는 값이다.
e.p = "NEW"
//result -> NEW has been assigned to ‘p’ in Example@33a17727.
표준 위임 (Standard Delegates)
Kotlin은 기본적으로 유용한 위임 팩토리 메소드를 제공한다.
Lazy
lazy()는 람다를 받아 Lazy의 인스턴스를 반환하는 함수로 lazy()에 전달된 람다를 실행하고 그 결과를 저장해둔다.
이후 get()을 호출하면 저장된 결과가 반환된다.
val lazyValue: String by lazy{
println("computed!")
"Hello"
}
fun main(args: Array){
println(lazyValue)
println(lazyValue)
}
//result -> computed!
// Hello
// Hello
lazy 프로퍼티 연산은 기본적으로 동기화된 처리이고, 값은 하나의 스레드에서만 계산되며 모든 스레드는 동일한 값을 볼 수 있다.
초기화 대리자의 동기화 처리가 필요 없다면 LazyThreadSafetyMode.PUBLICATION을 매개 변수로 lazy() 함수에 전달해 여러 스레드가 동시에 실행할 수 있도록 한다.
항상 단일 스레드에서만 초기화하는 것을 보장하려면 LazyThreadSafetyMode.NONE 모드를 사용할 수 있다.
이 모드는 스레드 안전성 보장 및 오버 헤드가 발생하지 않는다.
Observable
Delegates.observable() 은 초기 값과 수정을 위한 핸들러를 두 개의 인자로 가진다.
핸들러는 프로퍼티에 값을 할당할 때마다 할당이 먼저 수행된 후 호출된다.
세 개의 매개 변수는 순서대로 할당된 프로퍼티, 이전 값, 새로운 값이다.
import kotlin.properties.Delegates
class User{
var name: String by Delegates.observable(""){
prop, old, new ->
println("$old -> $new")
}
}
fun main(args: Array){
val user = User()
user.name = "first"
user.name = "second"
}
//result -> -> 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) // John Doe
println(user.age) // 25
읽기 전용 Map 대신에 MutableMap을 사용하면 var 프로퍼티에서도 동작한다.
class MutableUser(val map: MutableMap<String, Any?>){
var name: String by map
var age: Int by map
}
로컬 위임 프로퍼티(Local Delegated Properties)
로컬 변수를 위임된 속성으로 선언할 수 있다. 예를 들면 로컬 변수를 Lazy 하게 만들 수 있다.
fun example(computeFoo: () -> Foo) {
val memorizedFoo by lazy(computeFoo)
if (someCondition && memorizedFoo.isValid()){
memorizedFoo.doSometing()
}
}
memorizedFoo 변수는 첫 번째 접근에서만 계산되고, someCondition이 실패하면 계산되지 않는다.
프로퍼티 대리자 조건(Property Delegate Requirements) (소제목)
객체를 위임하기 위한 요구 사항을 알아보자.
읽기 전용 프로퍼티(val)의 경우 대리자는 getValue라는 함수를 제공해야 하며, 다음 매개 변수를 사용한다.
- thisRef - 프로퍼티 소유자와 같거나 상위 타입이어야 한다.
- property - 타입은 KProperty <*> 또는 그 슈퍼 타입이어야 한다.
이 함수는 프로퍼티와 동일한 타입(또는 하위 타입)을 반환해야 한다.
getValue() 또는 setValue() 함수는 위임 클래스의 멤버 함수 또는 확장 함수로 제공될 수 있다.
확장 함수로 제공될 경우는 이러한 기능을 제공하지 않는 객체에 속성을 위임해야 할 때 편리하다.
두 기능 모두 operator 키워드로 표시해야 한다.
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 타입의 반사 객체이다.
'Kotlin' 카테고리의 다른 글
[Kotlin] CoroutineBuilder (0) | 2022.05.12 |
---|---|
[Kotlin] Object (0) | 2022.03.25 |
[Kotlin] Delegation (0) | 2022.03.22 |
[Kotlin] 코틀린의 시퀀스(Sequence) (0) | 2022.01.24 |
[Kotlin] 코틀린에서 RxJava와 Coroutine (0) | 2022.01.06 |