본문 바로가기

Kotlin

Kotlin - Delegated properties

아래와 같은 몇가지 종류의 속성들은 필요할 때 마다 매번 구현 하는 것 보다 한번 구현해서 라이브러리에 추가하고 필요할 때 마다 재 사용하는 것이 편하다.

  • Lazy 속성: 속성에 처음 접근할때 그 값이 계산된다.
  • Observable 속성: 이 속성 값이 변경되면 리스너들이 벼녀경 내용에 대해 알게 된다.
  • 개별 필드에 저장하지 않고 map 에 저장하는 경우

이런 경우들을 위해 Kotlin에서는 delegated 속성을 지원한다.

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

“val/var <속성명>: <타입> by <표현식>” 형태로 작성한다.
표현식은 위임받는 클래스로 어떤 인터페이스를 구현해야하지는 않지만 get() 역할을 하는 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.")
	}
}

위 예에서 p 의 값을 읽을때 Delegate 객체의 getValue()가 호출이 된다. Delegate 클래스의 getValue 함수의 첫번재 파라미터는 p 값을 읽는 객체를 의미하고, 두번재 파라미터는 p의 정보를 의미한다.

val e = Example()
e.p = “NEW” // Example@3eb07fd3, thank you for delegating 'p' to me!
println(e.p) // NEW has been assigned to 'p' in Example@3eb07fd3.

Standard delegates

코틀린 기본 라이브러리는 몇가지 유용한 델리게이트 생성 팩토리 메소드를 제공 한다.

Lazy properties

lazy() 함수는 람다식을 받아서 Lazy<T> 객체를 반환하는 함수 이다. Lazy<T> 위임자는 lazy 속성을 구현했고, 첫번째 get()함수가 실행될때 lazy()함수에 전달했던 람다식을 실행하고 저장된 값을 반환한다. 두번째 get()을 호출할 때 부터는 저장된 값을 반환한다.

기본적으로 lazy 속성의 평가는 동기(synchronized)로 동작한다. 계산과정은 한개의 쓰레드에서만 이루어지며, 모든 쓰레드에서 동일한 값을 확인할 수 있다. 모든 쓰레드에서 계산과정을 수행할 수 있도록 설정하고 싶다면 (동기화가 필요없다면) lazy()함수에 LazyThreadSafetyMode.PUBLICATION 값을 파라미터로 전달하면 된다. 쓰레드 세이프를 보장 할 필요가 없거나 관련 오버헤드를 발생시키고 싶지 않다면 LazyThreadSafetyMode.NONE 값을 파라미터로 넘기면 된다. 

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

fun main() {
    println(lazyValue)
    // computed!
    // Hello

    println(lazyValue)
    // Hello
}

Observable properties

Delegates.observable() 함수는 초기 값과 handler(값이 변경되었을때 실행) 두개의 파라미터를 받는다.

handler는 속성의 값이 할당된 이후에 실행되고, 3개의 파라미터 (속성 정보(KProperty), 할당되기 이전 값, 새로운 값)를 받는다.

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

fun main() {
    val user = UserForDelegate()
    user.name = "first" // [name] <no name> -> first
    println(user.name) // first
    user.name = "second" // [name] first -> second
    println(user.name) // second
}

속성에 값이 할당되기 이전에 동작하는 vetoable() 대리자(delegate)를 사용하면 된다.

class UserForDelegatesVetoable {
    var name: String by Delegates.vetoable("<no name>") { prop, old, new ->
        println("[${prop.name}] $old -> $new")
        new.length <= 5// 5글자 이하값만 적용함
    }
}

fun main() {
    val user = UserForDelegatesVetoable()
    user.name = "first" // [name] <no name> -> first
    println(user.name) // first
    user.name = "second value" // [name] first -> second
    println(user.name) // first
}

 

다른 속성에 위임하기 (Delegating to another property)

다른 속성에 해당 속성의 getter와 setter를 위임 할 수 있다. 이러한 위임은 아래 3가지 경우에 가능하다.

  • 최상위 속성 (top-level property)
  • 같은 클래스의 멤버 나 확장 속성
  • 다른 클래스의 멤버나 확장 속성

속성을 다른 속성에 위임하기 위해서는 위임 속성의 이름에 "::" 를 사용한다.

속성에 속성을 위임하는 경우는 아래 예제처럼 속성 이름을 변경하고 싶은데 이미 사용자들이 사용하고 있는 경우 한번에 이름을 변경하면 기존 코드와 호환이 되지 않아 오류가 발생하는데 이를 방지하기 위한 중간 단계에 사용하면 유용하다.

class MyClassForDelegates {
    // oldName을 newName으로 대체함
    @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
    var oldName: Int by this::newName
    var newName: Int = 0
}

fun main() {
    val myClass = MyClassForDelegates()
    myClass.oldName = 10
    println(myClass.newName) // 10
}

 

Map 에 속성 저장 하기(Storing properties in a map)

JSON을 파싱하거나 동적 처리 작업을 위해 map 에 값을 저장하는 경우가 있다. 이런 경우 map 인스턴스에 속성들을 위임하여 처리 할 수 있다. map의 key 와 동일한 이름의 속성에 값이 할당 된다.

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

fun main() {
    val map = mapOf(
        "name" to "John Doe",
        "age" to 25,
        "num" to 12,
        "someString" to "String Value"
    )
    val user = UserForDelegatesMap(map)

    println(map.get("name")) // John Doe
    println(user.name) // John Doe

    println(map.get("age")) // 25
    println(user.age) // 25
}

 

위임한 지역 속성 (Local delegated properties)

아래 예제처럼 지역변수를 위임된 속성으로 선언 할 수 있다. 아래 예제의 경우에는 someCondition 이 true 인 경우에만 memoizedFoo 값이 계산되어(lazy) 불필요한 계산을 방지 할 수 있다.

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

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

 

속성 위임을 위한 요구사항 (Property delegate requirements)

읽기전용 속성(val)의 경우 대리자는 getValue() 함수를 제공해야 하며 이 함수는 아래 파라미터들을 받는다.

  • thisRef - 속성 소유자와 동일한 타입이거나 상위 타입 (확장 속성의 경우 확장되는 타입)
  • property - KProperty<*> 이거나 그 상위 타입

getValue()는 반드시 속성과 동일하거나 subtype을 반환해야 한다.

class Resource

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

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

가변 속성(var)의 경우, 대리자는 getValue() 함수 외에 setValue() 함수를 제공해야 하며, 이 함수는 아래의 파라미들을 받는다.

  • thisRef - 속성 소유자와 동일한 타입이거나 상위 타입 (확장 속성의 경우 확장되는 타입)
  • property - KProperty<*> 이거나 그 상위 타입
  • value - 속성과 동일하거나 subtype
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 키워드로 표시 되어야 하며, 대리 클래스의 함수로 제공 되거나, 확장 함수로 제공 될 수 있다. 원래 이런 기능을 제공하지 않는 대리자에 위임하는 경우 확장 함수로 제공하는 것이 더 편리하다. 

꼭 새로운 클래스를 정의하지 않고 코틀린 표준 라이브러리 인 ReadOnlyProperty와 ReadWriteProperty 인터페이스를 구현함 으로서 익명 오브젝트를 대리자를 정의 할 수 있다.

 

 

'Kotlin' 카테고리의 다른 글

Kotlin - 함수 (Functions)  (0) 2024.04.09
Kotlin - 타입 별칭 (Type aliases)  (0) 2024.03.29
Kotlin - Object 표현과 정의  (0) 2024.03.23
Kotlin - Deligation  (0) 2024.03.23
Kotlin - 인라인 값 클래스 (inline value classes)  (0) 2024.03.05