위임된 프로퍼티 (Delegated properties)
Kotlin은 특정 객체에 대한 프로퍼티의 set과 get 메서드의 호출을 위임할 수 있는 Delegated properties 메커니즘을 제공합니다.
이 경우 delegate 객체에는 getValue 메서드가 있어야 합니다. 변경 가능한 프로퍼티의 경우 setValue도 필요합니다.
import kotlin.reflect.KProperty
class Example {
var p: String by Delegate() // 1
override fun toString() = "Example Class"
}
class Delegate() {
operator fun getValue(thisRef: Any?, prop: KProperty<*>): String { // 2
return "$thisRef, thank you for delegating '${prop.name}' to me!"
}
operator fun setValue(thisRef: Any?, prop: KProperty<*>, value: String) { // 2
println("$value has been assigned to ${prop.name} in $thisRef")
}
}
fun main() {
val e = Example()
println(e.p)
e.p = "NEW"
}
- String 유형의 속성 p를 Delegate 클래스의 인스턴스에 위임합니다. delegate 객체는 by 키워드 뒤에 정의됩니다.
- 위임 메소드(Delegation Method). 이러한 메서드의 파라미터 또는 형식은 예제와 비슷합니다. 구현에는 필요한 단계가 포함될 수 있습니다. 불변 속성의 경우 getValue만 필요합니다.
일반적인 종류의 프로퍼티들이 있습니다. 필요할 때마다 수동으로 구현할 수 있지만 한 번 구현하고 라이브러리에 추가하면 도움이 됩니다. 예는 다음과 같습니다.
게으른 속성: 값은 처음 액세스할 때만 계산됩니다. 관찰 가능한 속성: 수신기는 이 속성의 변경 사항에 대해 알림을 받습니다. 각 속성에 대한 별도의 필드 대신 맵에 속성을 저장합니다.
표준 대리자 (Standard Delegates)
Kotlin 표준 라이브러리는 몇 가지 유용한 종류의 대리자를 위한 팩토리 메서드를 제공합니다.
Lazy properties
lazy()는 람다를 가지고 lazy 프로퍼티을 구현하기 위한 대리자 역할을 할 수 있는 Lazy<T>의 인스턴스를 반환하는 함수입니다.
get()에 대한 첫 번째 호출은 lazy()에 전달된 람다를 실행하고 결과를 기억하며, get()에 대한 후속 호출은 단순히 기억된 결과를 반환합니다.
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main() {
println(lazyValue)
println(lazyValue)
}
// 호출 결과
// computed!
// Hello
// Hello
기본적으로 지연 프로퍼티의 값은 동기화됩니다. 값은 하나의 스레드에서만 계산되고 모든 스레드는 동일한 값을 보게 됩니다. 초기화 대리자의 동기화가 필요하지 않고, 여러 스레드가 동시에 실행할 수 있도록 하려면 LazyThreadSafetyMode.PUBLICATION을 lazy() 함수에 매개변수로 전달합니다.
초기화가 항상 프로퍼티를 사용하는 스레드와 동일한 스레드에서 발생한다고 확신하는 경우 LazyThreadSafetyMode.NONE을 사용할 수 있습니다. 스레드 안전 보장 및 관련 오버헤드가 발생하지 않습니다.
Observable properties
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"
}
// 출력 결과
// <no name> -> first
// first -> second
할당을 가로채 거부하려면 observable() 대신 vetoable()을 사용하세요. vetoable에 전달된 핸들러는 새 속성 값을 할당하기 전에 호출됩니다.
다른 프로퍼티에 위임하기
하나의 프로퍼티는 자신의 getter 및 setter를 다른 프로퍼티에 위임할 수 있습니다. 이러한 위임은 최상위 및 클래스 프로퍼티(멤버 및 확장) 모두에 사용할 수 있습니다. 대리자 프로퍼티는 다음과 같을 수 있습니다.
- 최상위 프로퍼티
- 같은 클래스의 멤버 또는 확장 프로퍼티
- 다른 클래스의 멤버 또는 확장 프로퍼티
프로퍼티를 다른 프로퍼티에 위임하려면 대리자 이름에 적절한 :: 한정자를 사용해야 합니다.(예: 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 파싱 또는 기타 "동적" 작업 수행과 같은 애플리케이션에서 자주 발생합니다. 이 경우 맵 인스턴스 자체를 위임된 프로퍼티의 대리자로 사용할 수 있습니다.
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)
지역 변수를 위임된 프로퍼티로 선언할 수 있습니다. 예를 들어, 지역 변수를 lazy 하게 만들 수 있습니다.
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
memoizedFoo변수를 사용하려고 할 때만 작동됩니다. someCondition이 false이면 변수가 전혀 작동하지 않습니다.
프로퍼티 대리인 요구 사항
속성 위임에 대한 요구 사항은 다음과 같습니다.
읽기 전용 속성(val)의 경우 대리자는 다음 매개변수와 함께 연산자 함수 getValue()를 제공해야 합니다.
- thisRef는 속성 소유자와 같거나 상위 유형이어야 합니다(확장 속성의 경우 확장되는 유형이어야 함).
- 프로퍼티는 KProperty<*> 유형 또는 상위 유형이어야 합니다.
getValue()는 속성(또는 해당 하위 유형)과 동일한 유형을 반환해야 합니다.
class Resource
class Owner {
val valResource: Resource by ResourceDelegate()
}
class ResourceDelegate {
operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
return Resource()
}
}
가변 속성(var)의 경우 대리자는 다음 매개변수와 함께 연산자 함수 setValue()를 추가로 제공해야 합니다.
- thisRef는 속성 소유자와 같거나 상위 유형이어야 합니다(확장 속성의 경우 확장되는 유형이어야 함).
- 프로퍼티는 KProperty<*> 유형 또는 상위 유형이어야 합니다.
- 값은 속성(또는 해당 상위 유형)과 동일한 유형이어야 합니다.
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() 함수는 대리자 클래스의 멤버 함수 또는 확장 함수로 제공될 수 있습니다. 후자는 원래 이러한 기능을 제공하지 않는 객체에 속성을 위임해야 할 때 편리합니다. 두 함수 모두 operator 키워드로 표시해야 합니다.
참고 : 코틀린 공식문서
'Language > Kotlin' 카테고리의 다른 글
코틀린[Kotlin]에서 Delegation 패턴 사용하기 (0) | 2021.09.26 |
---|---|
코틀린의[Kotlin] Scope 함수 (0) | 2021.09.25 |
코틀린[Kotlin] Collections에서 자주 사용하는 함수 알아보기 (Filter, map, count, groupby ....) (0) | 2021.09.24 |
코틀린[Kotlin] 컬렉션(Collections) 알아보기 (List, Map, Set ...) (0) | 2021.09.24 |
코틀린[Kotlin] 함수형 프로그래밍 (고차 함수, 람다 함수, 확장 함수) (0) | 2021.09.22 |