Kotlin 공식 문서를 보고 정리했다. 공식 문서 참조.
property들의 값을 get(), set()으로 구현해야 하는 경우, 같은 형태의 property가 필요하면, 각 property마다 get(), set()을 구현해야 한다. 이런 경우, 한번만 구현해서 중복을 제거하면 좋을 것이다. 이는 delegate를 통해 가능해진다. 예를 들면 다음과 같은 케이스들이 존재한다.
- lazy properties : 값을 처음 가져올 때, 생성 및 계산이 이루어짐.
- observable properties : 값의 변경시 listener들이 알림을 받음.
- storing properties in a map : property들의 값을 map에 저장해놓고 가져오는 경우.
이런 경우들을 위해, Kotlin에서는 delegated properties를 지원한다.사용법은 다음과 같다.
class Example {
// syntax : val/var <property name>: <Type> by <expression>
var p: String by Delegate()
}
‘by’ 키워드가 사용되고 property의 get(), set()은 Delegate 클래스의 getValue(), setValue()에게 위임(delegated)된다. Delegate 클래스는 다음과 같다.
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.")
}
}
Standard delegates
Kotlin standard library에서는 유용한 delegate들을 표준으로 제공한다.
Lazy
lazy()는 함수이다. 표준 라이브러리문서를 찾아보면 다음과 같다.
fun <T> lazy(initializer: () -> T): Lazy<T>
처음 get()이 호출될 때, 기술한 람다함수가 실행되고 property에 저장된다. 그 다음 부터 get()이 호출되면, 저장된 값이 불려지게 된다.
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main() {
println(lazyValue)
println(lazyValue)
}
computed!
Hello
Hello
Observable
Property에 대한 observer 패턴은 자주 사용된다. 이 경우, Delegates.observable()을 사용하면 편해진다. 레퍼런스를 찾아보면, 다음과 같이 정의된다.
inline fun <T> observable(
initialValue: T,
crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit
): ReadWriteProperty<Any?, T>
첫번째 파라미터는 기본값을 받고, 두번째는 onChange에 해당하는 함수를 받는다. property, old, new 3개의 argument를 갖게 되는데, 값 변경시 호출되는 부분이다. 실제 구현은 다음과 같다.
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"
}
보면 알 수 있듯이, observer 패턴 전체가 구현되진 않고, observer 패턴이 필요한 경우, 쉽게 구현할 수 있도록 해준다.
Delegating to another property
Kotlin 1.4 부터 다른 property의 getter, setter로 delegate가 가능하다. delegate 받는 property 앞에 “::” 를 붙여주어 사용한다. 예제는 다음과 같다.
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
공식문서에서 좋은 예를 보여주는데, property 이름을 변경하면서 이전 버전의 호환성을 유지하는 경우이다. 이전 property의 구현을 새 propery에게 위임해주면된다. 예제는 다음과 같다.
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
}
Storing properties in a map
앞서 말했듯, map 에 property들을 저장하는 경우, 사용할 수 있다. 이 경우, delegate 대상으로 map의 인스턴스를 직접 사용하면된다. 이렇게 구현시, property를 참조하면 map으로부터 값을 가져온다. 예제는 다음과 같다.
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
생성자에 map이 정의되어 있고, property인 name과 age는 map에 delegate하고 있다. User 인스턴스를 하나 생성해보자.
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
map으로 초기화 시켰지만, 사용은 propery로 이용가능하다.
println(user.name) // Prints "John Doe"
println(user.age) // Prints 25
추가적인 내용이 더 있지만, 당장은 이정도면 충분하여 생략한다. 처음 언급한대로 공식문서를 정리한 내용이니 이를 참고하면된다.