Why can you rewrite Code C into Code D, but not Code A into Code B? The difference, as the answer your linked also mentioned, is that toPx in Code A is an extension method of Dp declared in Density. It is declared like this:
open fun Dp.toPx(): Float
On the other hand, none of the properties you use in the with block in Code C are extension properties declared in Person.
So to call toPx, not only do you need an instance of Density as the dispatch receiver, you also need an instance of Dp as the extension receiver. (See this for more info about dispatch vs extension receivers)
With scope functions that change what the implicit receiver this means (e.g. with/run), you can easily provide both of these receivers:
with(LocalDensity.current) { // LocalDensity.current: (implicit) dispatch receiver
16.dp.toPx() // 16.dp: explicit extension receiver
}
It is possible (but not very intuitive or convenient) to provide both receivers without using scope functions. For example, and for academic purposes only, you can declare additional extension functions on Density:
fun Density.dpToPx(x: Int) = x.dp.toPx()
fun Density.dpToPx(x: Float) = x.dp.toPx()
fun Density.dpToPx(x: Double) = x.dp.toPx()
Here the dispatch receiver of x.dp.toPx is the same as the extension receiver of the newly declared Density.toPx, and the extension receiver is x.dp.
Then you can call this like this:
LocalDensity.current.dpToPx(16)
I strongly recommend you to use with when writing real code.