In the specification of value classes, it says:
A value class can only extend universal traits and cannot be extended itself. A universal trait is a trait that extends
Any
, only hasdef
s as members, and does no initialization. Universal traits allow basic inheritance of methods for value classes, but they incur the overhead of allocation. For example
trait Printable extends Any {
def print(): Unit = println(this)
}
class Wrapper(val underlying: Int) extends AnyVal with Printable
val w = new Wrapper(3)
w.print() // actually requires instantiating a Wrapper instance
Now, I would take this to mean that the following (probably) does not require instantiation:
trait Marker extends Any
class Wrapper(val underlying: Int) extends AnyVal with Marker {
def print(): Unit = println(this) //unrelated to Marker
}
val w = new Wrapper(3)
w.print() //probably no instantiation as print is unrelated to Marker
Am I correct?
And I would think there is an even chance as to whether this requires instantiation or not:
trait Printable extends Any {
def print(): Unit //no implementation
}
class Wrapper(val underlying: Int) extends AnyVal with Printable {
override def print() = println(this) //moved impl to value class
}
val w = new Wrapper(3)
w.print() // possibly requires instantiation
On the balance of probability, I would also think that no instantiation would be needed - am I correct?
I'd not thought about the exact implementation of print()
in the example:
def print(): Unit = println(this)
Let's say that I used the following instead:
def print(): Unit = println(underlying)
Would these cause instantiations?
Am I correct?
No, we can see it if we emit the final compilation output with -Xprint:jvm
:
<synthetic> object F$Wrapper extends Object {
final def print$extension($this: Int): Unit =
scala.Predef.println(new com.testing.F$Wrapper($this));
This is due to the fact println
has a type signature requiring Any
, so we're shooting ourselves in the foot here since we're effectively "treating the value class ttpe as another type".
Although the call is dispatched to the static method call:
val w: Int = 3;
F$Wrapper.print$extension(w)
We're still incurring the allocation inside print$extension
.
If we stray away from using Wrapper.this
, then your first assumption is indeed correct and we can see the compiler happily unwrap Wrapper
:
<synthetic> object F$Wrapper extends Object {
final def print$extension($this: Int): Unit =
scala.Predef.println(scala.Int.box($this));
And the call site now looks like this:
val w: Int = 3;
com.testing.F$Wrapper.print$extension(w)
This is valid for both of your examples now, as there is no need for any dynamic dispatch on the created interface.