Home > front end >  Why does casting to a generic work without an instance of that type?
Why does casting to a generic work without an instance of that type?

Time:09-18

I've created 2 kotlin methods: one to check a type and another to cast an object. They look like:

fun Any?.isOfType(type: Class<*>): Boolean{
  return type.isInstance(this)
  // return `this is T` does NOT work.
}

and

fun <T> Any?.castToType(): T {
  return this as T
  // Works, albeit with a warning.
}

I've read some posts on generics and erasures, but I can't get over what seems to be a discrepancy.

Why is it that checking for the type of an object cannot be done with generics, but casting to a generic can?

CodePudding user response:

The question is why:

fun <T> Any?.castToType() = this as T     // compiles with warning

"hello".castToType<Int>()

"works" but this won't even compile:

fun <T> Any?.isOfType() = this is T      // won't compile

"hello".isOfType<Int>()

Actually both don't really work. In both cases the type is erased at runtime. So why does one compile and the other doesn't?

this is T cannot work at runtime since the type of T is unknown and thus the compiler has to reject it.

this as T on the other hand might work:

  "hello".castToType<Int>()            // no runtime error but NOP
  "hello".castToType<Int>().minus(1)   // throws ClassCastException
  2.0.castToType<Int>().minus(1)       // no runtime error, returns 1

In some cases it works, in others it throws an exception. Now every unchecked cast can either succeed or lead to runtime exceptions (with or without generic types) so it makes sense to show a warning instead of a compile error.

Summary

  • unchecked casts with generic types are no different from unchecked casts without generic types, they are dangerous but a warning is sufficient
  • type checks with generic types on the other hand are impossible at runtime

Addendum

The official documentation explains type erasure and why is-checks with type arguments can't succeed at runtime:

At runtime, the instances of generic types do not hold any information about their actual type arguments. The type information is said to be erased. For example, the instances of Foo and Foo<Baz?> are erased to just Foo<*>. Due to the type erasure, there is no general way to check whether an instance of a generic type was created with certain type arguments at runtime, and the compiler prohibits such is-checks such as ints is List or list is T (type parameter)

(https://kotlinlang.org/docs/generics.html#type-erasure)

In my own words: I can't check whether A is B if I don't know what B is. If B is a class I can check against an instance of that class (that's why type.isInstance(this) works) but if B is a generic type, the runtime has no information on it (it was erased by the compiler).

CodePudding user response:

This isn't about casting vs checking; it's about using generics vs class objects.

The second example is generic; it uses T as a type parameter. Unfortunately, because generics are implemented using type erasure, this means that the type isn't available at runtime (because it has been erased, and replaced by the relevant upper bound — Any? in this case). This is why operations such as type checking or casting to a type parameter can be unsafe and give compilation warnings.

The first example, though, doesn't use a type parameter; instead, it uses a parameter which is called type, but is a Class object, representing a particular class. This is a value which is provided at runtime, just like any other method parameter, and so you can call methods such as cast() and isInstance() to handle some type issues at runtime. However, they're closely related to reflection, and have some of the same disadvantages, such as fragility, ugly code, and limited compile-time checks.

(Kotlin code often uses KClass objects instead of Java Class objects, but the principle is the same.)


It may be worth highlighting the difference between class and type, which are related but subtly different. For example, String is both a class and a type, while String? is another type derived from the same class. LinkedList is a class, but not a type (because it needs a type parameter); LinkedList<Int> is a type.

Types can of course be derived from interfaces as well as from classes, e.g. Runnable, or MutableList<Int>.

This is relevant to the question, because generics use type parameters, while Class objects represent classes.

  • Related