English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
대리 모델은 소프트웨어 설계 패턴 중 하나의 기본 기술입니다. 대리 모델에서는 두 개의 객체가 동일한 요청을 처리하며, 요청을 받은 객체는 요청을 다른 객체에 위임하여 처리합니다.
Kotlin은 대리 모델을 직접 지원하여 더욱 우아하고 간결합니다. Kotlin은 대시드 키워드 by를 통해 대리를 구현합니다.
클래스 대리는 하나의 클래스에서 정의된 메서드가 실제로는 다른 클래스의 객체의 메서드를 호출하여 구현된다는 것입니다.
다음 예제에서는 파생 클래스 Derived가 인터페이스 Base의 모든 메서드를 상속받고, 전달된 Base 클래스의 객체를 통해 이 메서드를 실행합니다.
// 인터페이스를 생성합니다 interface Base { fun print() } // 이 인터페이스를 구현한 대리 클래스 class BaseImpl(val x: Int) : Base { override fun print() { print(x) } } // 대시드 키워드 by를 통해 대리 클래스를 구축합니다 class Derived(b: Base) : Base by b fun main(args: Array<String>) { val b = BaseImpl(10) Derived(b).print() // 출력 10 }
Derived 선언에서, by 문은 b를 Derived의 객체 예제 내부에 저장하며, 컴파일러는 Base 인터페이스를 상속한 모든 메서드를 생성하고, 호출은 b로 전달됩니다.
속성 위임은 클래스의 특정 속성 값이 클래스 내에서 직접 정의되지 않고, 대신 대리 클래스에 위임되어 클래스의 속성을 일관되게 관리하는 것을 의미합니다.
속성 위임 문법 형식:
val/var <속성 이름>: <타입> by <표현식>
var/val: 속성 타입(변수)/읽기 전용)
속성 이름: 속성 이름
타입: 속성 데이터 타입
표현식: 위임 대리 클래스
by 키워드 이후의 표현식은 위임이며, 속성의 get() 메서드(또는 set() 메서드)는 이 객체의 getValue()과 setValue() 메서드에 위임됩니다. 속성 위임은 어떤 인터페이스도 구현하지 않아도 됩니다만, getValue() 함수(변수 속성의 경우에는 setValue() 함수)를 제공해야 합니다.
이 클래스는 getValue() 메서드와 setValue() 메서드를 포함해야 하며, thisRef는 위임을 하는 클래스의 객체이고, prop는 위임을 하는 속성의 객체입니다.
import kotlin.reflect.KProperty // 속성 위임을 포함한 클래스 정의 class Example { var p: String by Delegate() } // 위임된 클래스 class Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "$thisRef, 여기서 ${property.name} 속성을 위임했습니다" } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("$thisRef의 ${property.name} 속성이 $value로 할당됩니다") } } fun main(args: Array<String>) { val e = Example() println(e.p) // 이 속성에 접근하여 getValue() 함수 호출 e.p = "w3codebox" // setValue() 함수 호출 println(e.p) }
출력 결과는 다음과 같습니다:
Example@433c675d, 여기서 p 속성을 대리합니다 Example@433c675d의 p 속성이 w로 할당됩니다3codebox Example@433c675d, 여기서 p 속성을 대리합니다
Kotlin의 표준 라이브러리에는 속성 대리자를 구현하기 위한 많은 팩토리 메서드가 포함되어 있습니다.
lazy()는 함수로, Lambda 표현식을 매개변수로 받아 Lazy<T> 인스턴스를 반환하는 함수입니다. 반환된 인스턴스는 지연 속성을 구현하는 데 사용될 수 있습니다. 첫 번째 호출 get()에 대해 lazy()에 전달된 Lambda 표현식이 실행되고 결과가 기록됩니다. get() 호출이 반복될 때마다 기록된 결과만 반환됩니다.
val lazyValue: String by lazy { println("computed!") // 첫 번째 호출 출력, 두 번째 호출은 실행되지 않음 "Hello" } fun main(args: Array<String>) { println(lazyValue) // 첫 번째 실행, 두 번째 출력 표현식 실행 println(lazyValue) // 두 번째 실행, 반환 값만 출력 }
실행 출력 결과:
computed! Hello Hello
observable은观察자 패턴을 구현하는 데 사용될 수 있습니다.
Delegates.observable() 함수는 두 가지 매개변수를 받습니다: 첫 번째는 초기 값, 두 번째는 속성 값 변경 이벤트 리스너(handler)입니다.
속성 할당 후 이벤트 리스너(handler)가 실행됩니다. 이 리스너는 세 가지 매개변수를 가집니다: 할당된 속성, 이전 값, 새 값:
import kotlin.properties.Delegates class User { var name: String by Delegates.observable("초기 값") { prop, old, new -> println("이전 값:$old -> 새 값:$new" } } fun main(args: Array<String>) { val user = User() user.name = "첫 번째 할당" user.name = "두 번째 할당" }
실행 출력 결과:
> 이전 값:초기 값 -> 새 값:첫 번째 할당 > 이전 값:첫 번째 할당 -> 새 값:두 번째 할당
일반적인 사용 사례는 매핑(map) 안에 속성 값을 저장하는 것입니다. 이는 JSON을 분석하거나 다른 "동적" 작업을 수행하는 애플리케이션에서 자주 나타납니다. 이 경우, 대리 속성을 구현하기 위해 매핑 예제 자체를 대리자로 사용할 수 있습니다.
class Site(val map: Map<String, Any?>) { val name: String by map val url: String by map } fun main(args: Array<String>) { // 생성자는 하나의 매핑 매개변수를 받습니다 val site = Site(mapOf( "name" to "기본 튜토리얼 웹사이트", "url" to "ko.oldtoolbag.com" )) // 맵 값 읽기 println(site.name) println(site.url) }
실행 출력 결과:
기본 튜토리얼 웹사이트 ko.oldtoolbag.com
var 속성을 사용하면 Map 대신 MutableMap을 사용해야 합니다:
class Site(val map: MutableMap<String, Any?>) { val name: String by map val url: String by map } fun main(args: Array<String>) { var map:MutableMap<String, Any?> = mutableMapOf( "name" to "기본 튜토리얼 웹사이트", "url" to "ko.oldtoolbag.com" ) val site = Site(map) println(site.name) println(site.url) println("--------------) map.put("name", "Google") map.put("url", "www.google.com") println(site.name) println(site.url) }
실행 출력 결과:
기본 튜토리얼 웹사이트 ko.oldtoolbag.com -------------- Google www.google.com
notNull은 초기화 단계에서 속성 값을 결정할 수 없는 경우에 적합합니다.
class Foo { var notNullBar: String by Delegates.notNull<String>() } foo.notNullBar = "bar" println(foo.notNullBar)
주의해야 할 것은, 속성이 할당 전에 접근되면 예외가 발생할 수 있습니다.
지역 변수를 대리 속성으로 선언할 수 있습니다. 예를 들어, 지역 변수를 레지어 초기화할 수 있습니다:
fun example(computeFoo: ()) -> Foo) { val memoizedFoo by lazy(computeFoo) if (someCondition && memoizedFoo.isValid()) { memoizedFoo.doSomething() } }
memoizedFoo 변수는 첫 번째 접근 시에만 계산됩니다. someCondition이 실패하면, 이 변수는 결코 계산되지 않습니다.
읽기 전용 속성(즉 val 속성)의 대리자는 getValue()라는 함수를 제공해야 합니다. 이 함수는 다음 파라미터를 받아야 합니다:
thisRef - 속성 소유자 타입(확장 속성의 경우 확장 타입)과 동일하거나 그 범위 내 타입이어야 합니다
property - KProperty 또는 그 범위 내 타입이어야 합니다
이 함수는 속성과 동일한 타입(또는 그 서브 타입)을 반환해야 합니다.
가변(mutable) 속성(즉, var 속성)의 경우, getValue() 함수 외에도委托는 setValue()라는 추가 함수를 제공해야 합니다. 이 함수는 다음 매개변수를 받습니다:
thisRef - 속성 소유자 타입(확장 속성의 경우 확장 타입)과 동일하거나 그 범위 내 타입이어야 합니다
property - KProperty 또는 그 범위 내 타입이어야 합니다
new value - 속성과 동일한 타입이거나 그 범위 내 타입이어야 합니다.
각 위임된 속성 구현의 뒤에는 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::prop는 KProperty 타입의 반사 객체로, 이 객체는 prop 자체를 설명합니다.
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) // 대리자 생성 } 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 또는 그 상위 타입이어야 합니다.
MyUI 예제를 생성하는 동안 각 속성에 대해 provideDelegate 메서드를 호출하고 즉시 필요한 검증을 수행합니다.
이러한 방식으로 속성과 대리자 간의 바인딩을 차단할 수 있는 능력이 없다면, 동일한 기능을 구현하기 위해 명시적으로 속성 이름을 전달해야 하며, 이는 매우 편리하지 않습니다:
// 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) // 대리자 생성 }
생성된 코드에서는 provideDelegate 메서드를 호출하여 보조 prop$delegate 속성을 초기화합니다. 속성 선언 val prop: Type by MyDelegate()에서 생성된 코드와 비교하여(provideDelegate 메서드가 존재하지 않을 때 생성된 코드와) 다음과 같습니다:
class C { var prop: Type by MyDelegate() } // 이 코드는 "provideDelegate" 기능이 사용 가능할 때 // 컴파일러가 생성한 코드: class C { // “provideDelegate” 호출하여 추가 “delegate” 속성 생성 private val prop$delegate = MyDelegate().provideDelegate(this, this::prop) val prop: Type get() = prop$delegate.getValue(this, this::prop) }
请注意,provideDelegate 메서드는 보조 속성 생성에 영향을 미칩니다. getter 또는 setter로 생성된 코드에는 영향을 미치지 않습니다。