Why Array.copyOf() is not covariant?

Is there any reason copyOf() was declared as:

inline fun <T> Array<T>.copyOf(): Array<T>

And not as:

inline fun <T> Array<out T>.copyOf(): Array<T>

?

It would make possible to “cast” arrays like this:

val animals = dogs.copyOf<Animal>()

It would be also useful if we need to modify the data provided as vararg param:

fun foo(vararg strings: String) {
    val strings2 = strings.copyOf()
    strings2[0] = "foo"
}

I believe in this case it is safe to perform an unchecked cast, but it is much worse than above:

fun foo(vararg strings: String) {
    @Suppress("UNCHECKED_CAST")
    val strings2 = strings.copyOf() as Array<String>
    strings2[0] = "foo"
}

The implementation could be as simple as:

inline fun <reified T> Array<out T>.copyOf(): Array<T> {
    return Array(size) { this[it] }
}

Is reified a problem for multiplatform code? Or maybe this is to allow more efficient implementations that utilize direct memory copy?

2 Likes

Marking T reified would make it impossible to use copyOf in places where T is not known at compile time.

4 Likes

Ahh, that makes sense, thank you.

We thought about overloading copyOf function with a reified variant, so that it could be used both in reified and non-reified contexts, but unfortunately the current overloading rules in Kotlin don’t permit that.

2 Likes

What about another name like: copyOfTyped(), typedCopyOf() or toTypedArray()?

toTypedArray() may sound odd for copying, but it is consistent with Collection.toTypedArray() which does almost the same thing; and with functions like toList(), toMutableList(), toSet() that are very often used not for converting, but for copying. It would be very inconsistent with existing copyOf() though.

Anyway, it is very easy to implement when needed, so this is not that important.

Also note that in reality arrays aren’t quite covariant (although in Java they are at compile time, even if not at runtime - it is a dog’s breakfast due to starting out without generics).

Yes, I understand arrays are invariant by default. But that does mean they can’t be covariant if we only read from them. And when we copy the array, we only read from it.

But then nothing stops you from writing to it later, and getting an ArrayStoreException or whatever it’s called. Having covariant mutable arrays is just a bad design flaw in Java.

I’m not sure if we are on the same page:

    val dogs = arrayOf(Dog(), Dog())
    
    val animals: Array<Animal> = dogs // not allowed

    val animals: Array<out Animal> = dogs // allowed
    animals[0] = Cat() // not allowed

Yes, you can use array of dogs as covariant array of animals. And no, you can’t write to it. Therefore, I believe it is type-safe. Or do I miss something?

Of course, we can cast animals to Array<Animal> and then add a Cat, but then we have to perform unchecked cast, so we are warned that we do something potentially not type-safe.

Ah, I see. I forgot that Kotlin allows to declare variance explicitly.

But nothing stops you to do it anyway at declaration site:

val dogs = arrayOf(Dog(), Dog())
val copy: Array<out Animal> = dogs.copyOf()

But of course, you can’t now add other animals to the copy. Which would be possible if copyOf() was declared to accept Array<out T> and return Array<T>. Makes sense. Looks like a (minor) design flaw in the stdlib API to me.

Yes, it looks like a flaw in API and this is why I created this thread. Usually, when we only read from a collection, we use List (which is implicitly covariant) or Array<out T>, so I was surprised to see copyOf() doesn’t do this. But in fact, this is intentional and caused by the technical limitation in JVM.

To create an array in JVM we need to know its exact type. We can create List<T>, but we can’t create Array<T>. As long as T is invariant in copyOf(), we can use the type of the source array to create the destination array. If T is covariant, we no longer have any means to know T, so we would have to either provide Class<T> / KClass<T> or use a reified param. This is what I suggested, but I totally missed the fact that even the caller may not know T and then covariant copyOf() would be impossible to invoke.

Still, I think two separate functions would be great to have.

Yes, I see now what I missed. You can’t just create a copy of Array<Dog> and return it as Array<Animal>. You can safely return it as Array<out Animal>, and that’s what I originally thought was the intent. But if you want Array<Animal>, then yes, of course it has to be either a reified type…