I need to create a temporary Array[T | Null]
where T
is a type parameter with AnyRef
as upper bound.
Given the arrays implementation will use an Array[AnyRef]
after erasure for whatever T
is, why should I relay on a ClassTag
?
def method[T <: AnyRef](elements: Iterable[T])(using ctTorNull: ClassTag[T | Null]): T =
val array = elements.toArray[T | Null]
???
}
Is there a way to achieve the same without using ClassTag
nor asInstanceOf
in scala 3.4?
Edit: As Dmytro pointed out in his answer, the type information that the JVM has about Array[A]
and of Array[B]
is different even if both A and B extend AnyRef
.
Evidently, arrays hold more information about the declared type of their elements than many collections do. So, to avoid the ClassTag
in this scenario use a mutable collection that does not require a ClassTag
. mutable.ArraySeq
requires it but ArrayBuffer
does not.
The example below uses the IndexedSeq
factory which creates an ArrayBuffer
.
def method[T <: AnyRef](elements: Iterable[T]): T =
val array = scala.collections.mutable.IndexedSeq.from[T | Null](elements)
???
}
If for efficiency reasons you need that the temporary collection be an Array, you may avoid the contextual ClassTag parameter if the received collection exposes the ClassTag
of its backing Array. mutable.ArraySeq
and mutable.ArrayStack
do it in the elemTag
member.
def method[T <: AnyRef](elements: mutable.ArraySeq[T]): T =
val array = elements.toArray[T | Null](using elements.elemTag.asInstanceOf[ClassTag[T | Null]])(elements)
???
}
Here I assumed that the erased type of T
and T | Null
is the same when T <: AnyRef
.
Given the arrays implementation will use an
Array[AnyRef]
after erasure for whateverT
is
(where
T
is a type parameter withAnyRef
as upper bound)
It's not true that erasure of Array[T]
(T <: AnyRef
) is Array[AnyRef]
. See the spec:
The erasure of the parameterized type
scala.Array[T]
isscala.Array[|T|]
.
https://www.scala-lang.org/files/archive/spec/3.4/03-types.html#type-erasure
(Here |T|
denotes the erasure of T
).
You can check that Array[Int]
and Array[String]
have different classes at runtime
classOf[Array[Int]]) // class [I
classOf[Array[String]] // class [Ljava.lang.String;
.asInstanceOf
will throw if you specify wrong class
Array(1, 2, 3).asInstanceOf[Array[String]]
// java.lang.ClassCastException: class [I cannot be cast to class [Ljava.lang.String;
Similarly,
class SomeClass
class OtherClass
implicitly[SomeClass <:< AnyRef] // compiles
implicitly[OtherClass <:< AnyRef] // compiles
classOf[Array[SomeClass]]) // class [LSomeClass;
classOf[Array[OtherClass]] // class [LOtherClass;
Array(new SomeClass()).asInstanceOf[Array[OtherClass]]
// java.lang.ClassCastException: [LSomeClass; cannot be cast to [LOtherClass;
It's T
in Iterable[T]
that is erased. So you need ClassTag
to preserve the information about T
from compile time to runtime (in order to get correct Array[T]
at runtime).