Skip to content

Commit 4b13343

Browse files
author
Roman Venediktov
committed
[KEEP-0449] Publish KEEP
1 parent 378978b commit 4b13343

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
# Typed Delegate Access
2+
3+
- **Type**: Design Proposal
4+
- **Author**: Roman Venediktov
5+
- **Contributors**: Michail Zarečenskij
6+
- **Status**: Public discussion
7+
- **YouTrack Issue**: [KT-30631](https://youtrack.jetbrains.com/issue/KT-30631)
8+
9+
## Abstract
10+
11+
This proposal describes a way to access a typed value of a delegate within the private scope:
12+
```kotlin
13+
class C {
14+
val dbConnection by lazy { connectToDb() }
15+
16+
fun close() {
17+
// Access to the backing property delegate of type Lazy<DbConnection>
18+
if (::dbConnection.isInitialized()) {
19+
dbConnection.close()
20+
}
21+
}
22+
}
23+
```
24+
25+
## Motivation
26+
27+
### Expose wrapper API
28+
29+
Delegation is often used to hide wrapper logic when the wrapper mainly exposes a single property.
30+
It lets you use `user` instead of repetitive `user.value`, while still preserving behaviors like caching, lazy
31+
initialization, dependency injection, or observability.
32+
33+
However, delegates in Kotlin also hide the API of the wrapped object. The most straightforward example is `isInitialized()` of the
34+
`kotlin.Lazy` delegate. Today, if one wants to get access to the API of the wrapped object, it's required to store delegate separately
35+
or use reflection:
36+
```kotlin
37+
private val _dbConnection = lazy { connectToDb() }
38+
val dbConnection by _dbConnection
39+
if (_dbConnection.isInitialized()) {
40+
dbConnection.close()
41+
}
42+
```
43+
44+
This pattern shares the same drawbacks as backing fields: it requires additional names and adds boilerplate.
45+
46+
Other examples of this pattern include:
47+
48+
- An observable value that must be registered once and accessed many times.
49+
- A caching wrapper that needs to be cleared at specific moments but is usually accessed via a simple value property.
50+
- A lazy value that must be eagerly initialized under certain conditions.
51+
52+
Note that, by default, in all these cases the wrapper API should be available only to the holder of such a delegated
53+
property.
54+
55+
### Potential unification of `lateinit` and delegated properties
56+
57+
The second part of the story concerns the `lateinit var` feature. We acknowledge that in most cases, `lateinit var` follows
58+
a contract where a property is assigned once and never changed afterward. We would also like to extend this feature to
59+
work with primitive types. Additionally, to support dependency injection and multithreaded scenarios, we need a setter
60+
and synchronization logic.
61+
62+
One possible solution is to unify `lateinit var` with delegated properties and introduce an `assignOnce` delegate. Such a
63+
delegate could cover lateinit use cases without being restricted to non-primitive types. However, `lateinit` currently
64+
relies on an ad-hoc mechanism to check whether a property has been [initialized](https://github.com/JetBrains/kotlin/blob/2.3.0/libraries/stdlib/src/kotlin/util/Lateinit.kt#L22).
65+
To support this with delegates, we would need to expose a similar API to users. Conceptually, this is the same situation as `isInitialized()` in
66+
`kotlin.Lazy` but from a different angle.
67+
68+
Note that the specific details of this unification are not yet clear and are out of scope for this proposal.
69+
70+
## Proposal
71+
72+
The proposal is to resolve `::property` differently for delegated properties, allowing accessing the typed value of a delegate:
73+
74+
```kotlin
75+
class C {
76+
val dbConnection by lazy { connectToDb() }
77+
78+
fun close() {
79+
::dbConnection.delegate.also {
80+
if (it.isInitialized()) {
81+
it.value.close()
82+
}
83+
}
84+
}
85+
}
86+
```
87+
88+
The first step to achieve it is to introduce an additional set of types `KDelegatedPropertyN` and `KMutableDelegatedPropertyN`,
89+
which are parametrized by the type of the delegate and overrides the `getDelegate()` method with the proper return type.
90+
For example, for `KDelegatedProperty0`:
91+
92+
```kotlin
93+
interface KDelegatedProperty0<out V, out Delegate>
94+
: KProperty0<V>, KDelegatedProperty<Delegate> {
95+
public override fun getDelegate(): Delegate
96+
}
97+
```
98+
99+
The second step is to resolve `::property` to `KDelegatedPropertyN` for delegated properties.
100+
To prevent leaking implementation details, this new resolution will be available only inside the private scope of the property.
101+
This is similar to the resolution for [Explicit Backing Fields](https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0430-explicit-backing-fields.md), where private type is also accessible only inside the private scope of the property.
102+
Another analogue is a mutable property with a private setter.
103+
It is resolved to `KMutablePropertyN` inside the private scope and to `KPropertyN` outside.
104+
Moreover, the anonymous class generated in the outer scope, does not inherit `KMutablePropertyN`, so it is not possible to access the setter even with the downcast.
105+
See the [Behavior in different scopes](#behavior-in-different-scopes) section for more details.
106+
107+
## Details
108+
109+
### Requirements
110+
111+
The new resolution does not work for non-final delegated properties as they might be overridden with other delegates or just common properties.
112+
113+
### Resolution in inline functions
114+
115+
The resolution to `KDelegatedPropertyN` is disabled inside Public-API inline functions (with `public`, `protected` and `@PublishedApi internal` visibility).
116+
This is required to prevent leaking implementation details.
117+
This behavior is the same as for Explicit Backing Fields.
118+
119+
### New hierarchy of `KProperty`
120+
121+
The proposed new hierarchy of `KProperty` is as follows:
122+
123+
```kotlin
124+
// Existing interfaces:
125+
126+
public interface KProperty<out V> : KCallable<V>
127+
128+
public interface KMutableProperty<V> : KProperty<V>
129+
130+
public interface KProperty0<out V> : KProperty<V>, () -> V {
131+
public fun getDelegate(): Any?
132+
}
133+
134+
public interface KMutableProperty0<V> : KProperty0<V>, KMutableProperty<V>
135+
136+
public interface KProperty1<T, out V> : KProperty<V>, (T) -> V {
137+
public fun getDelegate(receiver: T): Any?
138+
}
139+
140+
public interface KMutableProperty1<T, V>
141+
: KProperty1<T, V>, KMutableProperty<V>
142+
143+
public interface KProperty2<D, E, out V> : KProperty<V>, (D, E) -> V {
144+
public fun getDelegate(receiver1: D, receiver2: E): Any?
145+
}
146+
147+
public interface KMutableProperty2<D, E, V>
148+
: KProperty2<D, E, V>, KMutableProperty<V>
149+
150+
// New interfaces:
151+
152+
public interface KDelegatedProperty<out V, out Delegate> : KProperty<V>
153+
154+
public interface KDelegatedProperty0<out V, out Delegate>
155+
: KProperty0<V>, KDelegatedProperty<V, Delegate> {
156+
public override fun getDelegate(): Delegate
157+
}
158+
159+
public interface KMutableDelegatedProperty0<V, out Delegate>
160+
: KDelegatedProperty0<V, Delegate>, KMutableProperty0<V>
161+
162+
public interface KDelegatedProperty1<T, out V, out Delegate>
163+
: KProperty1<T, V>, KDelegatedProperty<V, Delegate> {
164+
public override fun getDelegate(receiver: T): Delegate
165+
}
166+
167+
public interface KMutableDelegatedProperty1<T, V, out Delegate>
168+
: KDelegatedProperty1<T, V, Delegate>, KMutableProperty1<T, V>
169+
170+
public interface KDelegatedProperty2<D, E, out V, out Delegate>
171+
: KProperty2<D, E, V>, KDelegatedProperty<V, Delegate> {
172+
public override fun getDelegate(receiver1: D, receiver2: E): Delegate
173+
}
174+
175+
public interface KMutableDelegatedProperty2<D, E, V, out Delegate>
176+
: KDelegatedProperty2<D, E, V, Delegate>, KMutableProperty2<D, E, V>
177+
```
178+
179+
The `getDelegate` method is currently available only for the JVM platform.
180+
The current proposal includes the addition of this method to the `KPropertyN` interfaces on other platforms
181+
182+
### Additional extension functions
183+
184+
To simplify the usages of the new API, the following additional extension functions are proposed:
185+
186+
```kotlin
187+
// Existing extensions:
188+
189+
/**
190+
* Returns the instance of a delegated **extension property**, or `null` if this property is not delegated.
191+
* Throws an exception if this is not an extension property.
192+
*/
193+
fun KProperty1<*, *>.getExtensionDelegate(): Any?
194+
195+
/**
196+
* Returns the instance of a delegated **member extension property**, or `null` if this property is not delegated.
197+
* Throws an exception if this is not an extension property.
198+
*
199+
* @param receiver the instance of the class used to retrieve the value of the property delegate.
200+
*/
201+
fun <D> KProperty2<D, *, *>.getExtensionDelegate(receiver: D): Any?
202+
203+
// New extensions:
204+
205+
fun <Delegate> KDelegatedProperty1<*, *, Delegate>.getExtensionDelegate(): Delegate
206+
207+
fun <D, Delegate> KDelegatedProperty2<D, *, *, Delegate>.getExtensionDelegate(receiver: D): Delegate
208+
209+
@InlineOnly
210+
inline val <Delegate> KDelegatedProperty0<*, Delegate>.delegate: Delegate
211+
get() = getDelegate()
212+
213+
@InlineOnly
214+
inline val <T> KDelegatedProperty0<T, Lazy<T>>.isInitialized: Boolean
215+
get() = delegate.isInitialized()
216+
217+
```
218+
219+
### Inlining optimizations
220+
221+
Generation of an additional class for `KDelegatedPropertyN` just to access the delegate might be an undesired performance overhead.
222+
To overcome this, the compiler will try to inline these accesses if they occur on a statically known property in the scope of one function (after inlining).
223+
And if the property reference is not used for anything else, it will be eliminated.
224+
225+
### Behavior in different scopes
226+
227+
The proposed behavior in different cases of reflection is the same as for property with private setter.
228+
More precisely:
229+
230+
```kotlin
231+
class C {
232+
val prop by lazy { 42 }
233+
234+
fun foo1() {
235+
val tmp = ::prop // KDelegatedProperty0<Int, Lazy<Int>>
236+
val tmpDel: Lazy<Int> = tmp.getDelegate() // ok
237+
}
238+
239+
fun foo2(other: C) {
240+
val tmp = other::prop // KDelegatedProperty0<Int, Lazy<Int>>
241+
val tmpDel: Lazy<Int> = tmp.getDelegate() // ok
242+
}
243+
244+
fun foo3() {
245+
val tmp = C::prop // KDelegatedProperty1<C, Int, Lazy<Int>>
246+
val tmpDel: Lazy<Int> = tmp.getDelegate(C()) // ok
247+
}
248+
249+
fun leak(): KDelegatedProperty0<Int, Lazy<Int>> = ::prop
250+
}
251+
252+
fun C.leakFoo() {
253+
val tmp: Lazy<Int> = leak().getDelegate() // ok
254+
}
255+
256+
fun C.externalFoo1() {
257+
val tmp: KProperty0<Int> = ::prop
258+
val tmpDel: Any? = tmp.getDelegate() // JVM: IllegalAccessException, ok after `tmp.isAccessible = true`; Other platforms: ok
259+
tmp as KDelegatedProperty0<Int, Lazy<Int>> // ClassCastException
260+
}
261+
262+
fun C.externalFoo2() {
263+
val tmp: KProperty1<C, Int> = C::prop
264+
val tmpDel: Any? = tmp.getDelegate(this) // JVM: IllegalAccessException, ok after `tmp.isAccessible = true`; Other platforms: ok
265+
tmp as KDelegatedProperty1<C, Int, Lazy<Int>> // ClassCastException
266+
}
267+
268+
fun reflectionFoo() {
269+
val tmp: KProperty1<C, *> = C::class.declaredMemberProperties.first() // JVM only
270+
@Suppress("UNCHECKED_CAST")
271+
tmp as KDelegatedProperty1<C, Int, Lazy<Int>> // ok
272+
val tmpDel: Lazy<Int> = tmp.getDelegate(C()) // JVM: IllegalAccessException, ok after `tmp.isAccessible = true`; Other platforms: ok
273+
}
274+
```
275+
276+
This behavior is going to be achieved with the following compilation strategy:
277+
278+
- For property references inside the class, an anonymous class is generated as it is now.
279+
The only change is that this class will have a more precise supertype, and it will have `getDelegate` method overridden to prevent `IllegalAccessException`.
280+
- For property references outside the class, the behavior does not change.
281+
- For reflection (including special operator functions `getValue`, `setValue` and `provideDelegate`), the only difference is a more precise supertype of the returned value.

0 commit comments

Comments
 (0)