[Kotlin] Delegation and Delegated properties

코랑·2023년 4월 27일
0

android

목록 보기
7/16

정의

위임! 상속이냐 위임이냐~~
위임 페턴은 객체 지향을 지키면서도 상속에대한 좋은 대안이라는 것을 증명했다. 코틀린은 위임 구현을 위한 보일러플레이트 코드를 줄이기위해 지원하고있다.

interface Base {
    fun print()
}
// 일반 상속
class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}
// 위임
class Derived(b: Base) : Base by b

fun main() {
    val b = BaseImpl(10)
    Derived(b).print()
}

Derived의 슈퍼타입 리스트에 있는 by절은 b가 Derived 객체 내부에 저장되고, 컴파일러가 b로 전달되는 Base의 메소드를 모두 생성할 것

위임으로 구현된 인터페이스의 멤버 오버라이딩

interface Base {
    fun printMessage()
    fun printMessageLine()
}
// 인터페이스 구현
class BaseImpl(val x: Int) : Base {
    override fun printMessage() { print(x) }
    override fun printMessageLine() { println(x) }
}
// 구현된 멤버중 printMessage 오버라이딩
class Derived(b: Base) : Base by b {
    override fun printMessage() { print("abc") }
}

fun main() {
    val b = BaseImpl(10)
    Derived(b).printMessage() // 결과: abc
    Derived(b).printMessageLine() // 결과: 10
    // => 결과 abc10
}

근데 아래 방식으로 오버라이딩을 하면 인터페이스 멤버의 그 객체 내부 구현에만 액세스 할수 있기 때문에, 즉 Base 타입의 b를 위임한 Derived에서 오버라이딩을 해도 b 내부에서는 접근 할수가 없기 때문에(print 함수를 오버라이딩 하지 않는이상,,) 그래서 BaseImpl 내부 message가 프린트 됨

interface Base {
    val message: String
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override val message = "BaseImpl: x = $x"
    override fun print() { println(message) }
}

class Derived(b: Base) : Base by b {
    // This property is not accessed from b's implementation of `print`
    override val message = "Message of Derived"
}

fun main() {
    val b = BaseImpl(10)
    val derived = Derived(b)
    derived.print()
    println(derived.message)
}

위임 속성

일반적인 몇몇 속성 유형은 필요할 때마다 구현해서 쓰면되지만,
한 번 구현하여 라이브러리에 그걸 추가한 후, 나중에 재사용하는 것이 더 유용한 경우가 있음
예시)

  • Lazy 속성: 처음 접근 시에만 값이 계산됨(할당됨)
  • Observable속성: 이 속성의 변경사항을 알려주는 리스너
  • Map에 속성 저장: 각 속성을 분리된 필드 대신 맵으로 저장됨

위 세개 케이스를 다 커버하려고 코틀린은 아래와 같은 위임 속성을 지원한다~~

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

val/var <property name>: <Type> by <expression>
by 뒤에있는 expression은 위임이다. 왜냐면 property의 속성에 해당하는 get() set()이 getValue() setValue()로 위임되기 때문에
속성 위임은 인터페이스를 구현할필요는 없지만 getValue, setValue는 제공을 해야한다

import kotlin.reflect.KProperty
// by 키워드를 사용해 위임을 사용하려면, 이런식으로 getValue 와 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.")
    }
}
// 사용
val e = Example()
println(e.p)

// 결과값: Example@33a17727, thank you for delegating 'p' to me!

e.p = "NEW"
// 결과값: NEW has been assigned to 'p' in Example@33a17727

위 사용 예시처럼 Delegate 인스턴스를 위임하고 있는 e를 생성해서 p값을 읽으려고 할 때,
getValue가 호출이 되고, 첫번째 매개변수가 p를 읽은 객체이고, 두번째 매개변수가 p 자체에 대한 설명을 가지고 있음.(예시에서는 이름을 가지고오고있음.)

Standard delegates

코틀린에서 공식적으로 제공하는 몇가지 유용한 위임자들이 있음.

Lazy properties

lazy 함수는람다를 받고 Lazy<T>의 인스턴스를 반환한다.

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

fun main() {
    println(lazyValue) // 전달된 람다 모두 실행
    println(lazyValue) // get()에 대한 후속 호출은 단순히 기억된 결과 만을 반복함
}

// output 에서 computed!는 하나뿐임!
// computed!
// Hello
// Hello

기본적으로 지언 속성의 값은 동기화 되고. 값은 하나의 스레드에서만 계산되고 모든 스레드는 동일한 값을 보게 되기만 한다.

여러 스레드가 동시에 실행할 수 있도록 초기화 델리게이트의 동기화가 필요하지 않은 경우 LazyThreadSafetyMode.PUBLICATION을 lazy()에 매개 변수로 전달합니다.

초기화가 항상 프로퍼티를 사용하는 스레드와 동일한 스레드에서 이루어질 것이라고 확신하는 경우 LazyThreadSafetyMode.NONE을 사용할 수 있습니다. 이 경우 스레드 안전 보장 및 관련 오버헤드가 발생하지 않습니다.

observable properties

Delegates.onservable()는 두개의 매개변수를 받는데 하나는 초기값이고 다른 하나난 handler이다.

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

Delegating to another property

하나의 프로퍼티는 자신의 getter 및 setter를 다른 프로퍼티에 위임할 수 있습니다. 이러한 위임은 최상위 및 클래스 프로퍼티(멤버 및 확장) 모두에 사용할 수 있습니다. 대리자 프로퍼티는 다음과 같을 수 있습니다.

  • 최상위 속성
  • 같은 클래스의 멤버나 확장 속성
  • 다른 클래스의 멤버나 확장 속성
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
}

map에 property 저장

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

Local Delegated properties

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

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

Property delegate requirements

val(read only)로 선언하면 getValue만 구현해주면 된다.

class Resource

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

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

var(muttable)로 선언하면 getValue, setValue 모두 구현해줘야 된다.

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
        }
    }
}

Translation rules for delegated properties

class C {
    var prop: Type by MyDelegate()
}

// this code is generated by the compiler instead:
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)
}

위임 속성 최적화 케이스

// referenced property
class C<Type> {
    private var impl: Type = ...
    var prop: Type by ::impl
}
// named object
object NamedObject {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ...
}

val s: String by NamedObject
// 같은 모듈에 배킹 필드와 기본 getter를 가지고있는 final 값
val impl: ReadOnlyProperty<Any?, String> = ...

class A {
    val s: String by impl
}
// 상수, enum 속성, this, null
class A {
    operator fun getValue(thisRef: Any?, property: KProperty<*>) ...

    val s by this
}

다른 속성으로 위임될 때 변환 규칙

모든 위임 된 프로퍼티에 대한 후드 아래에서 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 이후에만 사용할 수 있다.

위임 제공?

provideDelegate 연산자 를 정의하여 속성 구현이 위임된 개체를 만들기 위한 논리를 확장할 수 있습니다. by 의 오른쪽에 사용된 객체가 provideDelegate 를 멤버 또는 확장 함수로 정의 하면 해당 함수가 호출되어 속성 대리자 인스턴스를 만듭니다.

provideDelegate 의 가능한 사용 사례 중 하나는 초기화시 속성의 일관성을 확인하는 것입니다.

예를 들어 바인딩하기 전에 속성 이름을 확인하려면 다음과 같이 작성할 수 있습니다.


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에 대해 생성된 코드에는 영향을 미치지 않는다.

0개의 댓글