Hello, I’m currently having an issue very similar to this post where my smart cast is failing. My setup is very similar and looks like this
class Foo<T : Fizz>(@JvmField val bar: T)
open class Fizz()
class Buzz() : Fizz() {
fun test()
}
In another module I’m calling this code
fun handleFoo(foo: Foo) {
if (foo.bar is Buzz) {
// Smart cast fails on this line
foo.bar.test()
}
}
I’ve read the original post and it makes a lot of sense why normally a val will not let you compile against it in this way because of how it can be overridden as a custom getter. However in my situation, I’m declaring bar with a @JvmField annotation in a final class. From my understanding, this should be fine since there is no way for the value to be overridden as a custom getter function.
I have also tried reimplementing Foo in Java like this:
public final class Foo<T extends Fizz> {
public final T bar;
public Foo(T bar) {
this.bar = bar;
}
}
I assume that this should work since being Java, everything is defined as it should be and there is no room for overriding with Kotlin stuff that can change between compilations.
Could anyone help me to understand why even the java implementation does not work?
I don’t think it has too much to do with overriding. The main point is that if the code is in another module, then the compiler can’t assume it will be exactly the same at runtime, as it is at the compile time. You compile against version 0.1 where the field is final, then at runtime you use version 0.2 where it is not final - ClassCastException.
Shouldn’t this be on the consumer of the module to handle? I’ve run into issues with signatures changing before and usually it’s the consumer’s fault for trying to use an out of date version. Though this doesn’t matter that much since I’m in control of both modules in this case.
Is there no way to tell the compiler to let me smart cast or guarantee that the field will not change?
You can create a local variable. Since you control the local variable, the compiler knows there can’t be any issues.
val bar = foo.bar
if (bar is Buzz) {
bar.test()
}
This works for all “X cannot be done because Y is defined in another module”, because their real cause is that you are accessing the same value multiple times, expecting it to be the same each time. By using a local variable, the value is only accessed once, so no matter the code on the other side, and no matter how it changes in the future, no bugs can appear.
This approach seems clunky and doesn’t make use of kotlin’s language features so it’s not entirely preferred. I fail to see how the value would change even when declaring in java as a final field which cannot be modified.
Well, it is the direct solution to the problem. The problem is accessing something external multiple times, assuming it won’t change in the meantime. You can’t control whether this is true or not. The only safe thing to do is to avoid the situation in the first place, by only accessing the value once.
Doesn’t it? It’s a safe-cast, the same as you tried to use. It uses a local variable to avoid querying a resource multiple times, that’s the most basic features of any programming language.
Who decides this? This is the only solution, this is how everyone else does this too.
Sure. But how do you know it’s a final field? You do not control the version of that other module. Dependency resolution could substitute it by another version of the other module, in which it isn’t final field. Your code would be bugged.
You can avoid an explicit local variable by using a scope function, such as with():
with (foo.bar) {
if (this is Buzz)
test()
}
or let():
foo.bar.let {
if (it is Buzz)
it.test()
}
In each case, the effect is exactly the same as a local variable (and may give the same bytecode), but avoiding the explicit declaration (and having to think of a suitable name).
This works well for short blocks, but can become unwieldy for long ones (as you can forget what it/this refers to). Ditto for nested blocks (which can be worked around in the case of let by giving an explicit name for the parameter in place of it, but then that’s just like an explicit local variable).