Safe cast operator (as?) throws exception

Hello!

fun <T> cast(from: Any): T? = from as? T

val x = cast<String>(42) // java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

Is it expected behaviour? I thought as? operator should suppress exception and return null.

Thanks!

2 Likes

Warning: Unchecked cast: Any to T

This is caused by type erasure.

Try:

inline fun <reified T> cast(from: Any): T? = from as? T
5 Likes

The question mark is usually used in Kotlin for nullability. The “as?” also allows the cast to return null when the left-hand side is null. See the documentation:

https://kotlinlang.org/docs/reference/typecasts.html#safe-nullable-cast-operator

It will not allow the above code.

Thank you @fvasco!

But this is simplified example of real code to show the problem.
I can’t use reified T in real code.

Thank you @Tim_van_der_Leeuw.1!

It is written in docs : To avoid an exception being thrown, one can use a safe cast operator as? that returns null on failure.
I thought that “failure” means not only case when left-side is null, for example this works well:

val x = 42 as? String // x initialised to null
2 Likes

Unless parameter T is reified, it’s treated as Any, so basically you wrote the following function: fun cast(from: Any): Any? = from as? Any. Obviously it works for anything and compiler adds a different check at call site which throws exception. Basically that’s limitation of type erasure inherited from JVM. In Java you would pass Class<T> instance, in Kotlin you can do the same or use reified modifier which will do the same under the hood,

Thank you @vbezhenar!

But I still don’t understand why this works:

val x = cast<String>("some str")
val x = cast<Int>(42)

What is more I thought that as? operator is some kinda shorthand for this:

fun <T> cast(from: Any): T? = try {
    from as T
} catch (e: ClassCastException) {
    null
}

Which works perfectly. Am I wrong?

The cast which throws the ClassCastException in your example is not the as? operator, it’s a cast inserted by the compiler where you assign the return value of a generic method (which is actually Any because of type erasure) to the variable of a specific type.

1 Like

@yole thanks for your reply!

If I understand you correctly, you tell that ClassCastException raised after the as? operator.
But at that time result should already be null.
Why ClassCastException message tells : java.lang.Integer cannot be cast to java.lang.String ?

No, the result will not be null because generic types are erased at runtime. Your cast function performs a cast to Any, which always succeeds.

2 Likes

Thank you @yole!

Now I understand. So there is no way to write such function, maybe if I pass KClass as argument?

It works if you use inline and reified type T. That prevents type-erasure.

The following worked from me in the Kotlin REPL:

inline fun <reified T> cast(any: Any?) : T? = any as? T?

cast<String>(14)
null

You can either use a reified type parameter or indeed pass KClass as an argument.

1 Like

Please clean up the documentation, the standard user (myself included):

  • I’ve got confused the first time I’ve read the docs
  • therefore I always forget what “as?” means, do I remember correctly
  • I go to the docs, get confused again
  • repeat

The docs is really-really wrong with the “on failure”. Just replace it and it will be ok.

Ah, and I’m resurrecting the 7 year old thread because… I’ve forgot what it means exactly, went to the docs, … :smiley:

What is your suggested improvement to docs?

I personally don’t see anything wrong with docs here. Most importantly, this behavior is not that much related to as? operator, but rather to generics. If you use as, you will get similar problems. And the compiler/IDE even clearly states the cast doesn’t work. So don’t ignore warnings and you should be fine (filterIsInstance is another story though :wink: )

edit:
This is explained here: Generics: in, out, where | Kotlin Documentation . Documentation doesn’t explicitly say casting to T is not effective in this case, however, it explains what is unchecked cast and it says we need a reified type parameter to safely cast to T.

1 Like

“as” has nothing to do with generics imho, I can use it without any generics in the whole code.

I never ignore warnings and even if I suppress it with an annotation I add a comment why that suppression is there.

The problem with docs is that “on failure” is ambiguous. Because there are two reasons for the failure: the instance is the wrong type or it is null.

Wrong type is a perfectly valid use case, I don’t get any warnings in this code:

interface A
class B : A

fun c(p : A) {
    val d = p as B
}

fun e(p : A?) {
    val d = p as B
    val d2 = d as? B
}

fun e2(p : Any) {
    val d = p as B
    val d2 = d as? B
}

From the documentation:

To avoid exceptions, use the safe cast operator as?, which returns null on failure.

So, does this avoid the cast exception as well? Of course not, and that is correct. However, the sentence above is very misleading as it indicates that I’ll avoid ALL exceptions. There is nothing that describes that it avoids NullPointerException only.

So, it would be better to say:

To avoid NullPointerException, use the safe cast operator as?, which returns null if the subject of the cast is null.

This is a much better explanation of what as? actually does.

I’m not sure what you mean to be honest. as? avoids both the NullPointerException and ClassCastException. It returns null for both null as? String and 5 as? String. What is the part in your code that shows otherwise or what is the problem in the code exactly?

1 Like

Well, you are right, it avoids ClassCastException as well, that’s my bad and my point exactly.

I was confused (again) went to the docs read this thread and arrived to the wrong conclusion. I simply can’t explain why, but I’m not the only one.

You could say that it’s my bad, and of course it is. But it would be so easy to help by adding a bit more information to the documentation.

Would it be really that hard to write it down explicitly? It’s like 2 sentences and it would be 100% clear what happens.

Also, one more thing. There is a warning when the cast can’t succeed, but there are cases when it can. Those cases are the confusing ones.

But I’m still not sure what would you like to clarify there. Just one comment ago you literally said the documentation says the operator does what it in fact really does. For some reason you expect a different behavior.

OP’s case was much different - it was due to generics.