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 에서 값을 읽으면 DelegategetValue() 함수를 실행한다. 이 메서드의 첫 번째 파라미터는 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가 포함된 ReadOnlyPropertyReadWriteProperty 인터페이스 중 하나를 구현할 수 있다. 이러한 인터페이스는 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에 대해 생성된 코드에는 영향을 미치지 않는다.

results matching ""

    No results matching ""