Anirudh Gupta
01/19/2025, 4:43 AMfun main() {
val vr = SnippetVR(object: Interaction {
override fun print() {
println("hello")
}
})
val snippet = vr.create()
snippet.print()
println("bye")
}
interface Interaction {
fun print()
}
class SnippetVR(val interaction: Interaction? = null) {
fun create(): Snippet {
return Snippet.newInstance().apply { this?.interaction = interaction }!!
}
}
class Snippet() {
companion object {
fun newInstance(): Snippet? = Snippet()
}
var interaction: Interaction? = null
fun print() {
interaction?.print()
}
}
On k1.9, the code outputs 'hello' as well as 'bye'
But on k2, 'hello' is not printed, only 'bye' is printed.
In k2, the interaction is being self assigned and not being picked from class variable.dmitriy.novozhilov
01/20/2025, 8:16 AMclass SnippetVR(val interaction: Interaction? = null) {
fun create(): Snippet {
return Snippet.newInstance().apply {
// this: Snippet?
this?.interaction = interaction
}!!
}
}
For the access to the interaction
in RHS of assignment, there are two candidates for where it could be resolved:
1. this@apply.interaction
2. this@SnippetVR.interaction
Since these different this
came from different scopes, the first one has more priority over the second one. But in your example there is a trick, that this@apply.interaction
is nullable. So K1 compiler tries to resolve this@apply.interaction
, sees that it has type Snippet?
, reports (under the hood) an error that this call is unsafe and looks for other candidates (eventually founding the second one).
But K2 compiler is smarter. It knows that we can execute the right hand side of a?.b = c
only in case when a != null
, so before resolving this part it adds smartcast that a != null
(this@Snippet != null
). This smartcast makes the first candidate compatible and everything successfully resolved to it.dmitriy.novozhilov
01/20/2025, 8:17 AMfun newInstance(): Snippet? = Snippet()
in your code to fun newInstance(): Snippet = Snippet()
, then both K1 and K2 will resolve to this@Snippet.interaction
, and only bye
will be printed in both casesAnirudh Gupta
01/23/2025, 11:47 AMAnirudh Gupta
01/23/2025, 11:51 AMfun main() {
val vr = SnippetVR(object: Interaction {
override fun print() {
println("1")
}
})
val snippet = vr.create()
snippet.print()
println("finish")
}
interface Interaction {
fun print()
}
class SnippetVR(val interaction: Interaction? = null) {
fun create(): Snippet {
return Snippet.newInstance().apply {
interaction?.print()
this?.interaction = interaction
interaction?.print()
}!!
}
}
class Snippet() {
companion object {
fun newInstance(): Snippet? = Snippet()
}
var interaction: Interaction? = object: Interaction {
override fun print() {
println("2")
}
}
fun print() {
interaction?.print()
}
}
Here, when developer writes the code:
interaction?.print()
this?.interaction = interaction
interaction?.print()
He would assume that same instance of interaction is being accessed (getter) in all three lines, but in reality, this is resolved like this:
interaction?.print() // prints 1
this?.interaction = interaction // sets interaction that prints 2
interaction?.print() // prints 1
dmitriy.novozhilov
01/27/2025, 8:43 AMSnippet?
)
In this scope there are two different this
, and they are arranged in the syntactical order (firstly resolved the closest one, then outer and so on). But because the closest receiver is Snippet?
the interaction
access is resolved to this.interaction
, which is an unsafe call. So the next thing the compiler is doing is trying to resolve the interaction
access on the outer this