https://kotlinlang.org logo
Title
d

dave08

01/29/2023, 3:46 PM
I have:
private val actions = mutableMapOf<String, SomeInterface>()

fun <T : SomeInterface> getOrNull(key: String): T? =
        actions[key] as? T

val tryToGetAFoo = getOrNull<Bar>("something")
// I have a Foo in the "something" key so this should be null... Bar and Foo are both implementation of SomeInterface...
what is this error that I'm getting:
class Bar cannot be cast to class Foo (Bar and Foo are in unnamed module of loader 'app')
?
c

Chris Lee

01/29/2023, 3:48 PM
Would be expected. A
Foo
is not a
Bar
even though they implement the same interface. The
as? T
combined with
getOrNull<Bar>
casts everything to a
Bar
.
d

dave08

01/29/2023, 3:49 PM
But in the docs it says that the
as?
operator returns null if it doesn't succeed in casting...
j

Joffrey

01/29/2023, 3:52 PM
And what does the compiler warning says on that line?
This cast is unchecked, meaning that at runtime there is no cast at all, so it won't behave as you expect
d

dave08

01/29/2023, 3:53 PM
class Bar cannot be cast to class Foo (Bar and Foo are in unnamed module of loader 'app')
c

Chris Lee

01/29/2023, 3:53 PM
In Kotlin playground: val tryToGetAFoo = getOrNull<Bar>(“something”)
d

dave08

01/29/2023, 3:53 PM
Right, that's exactly the error...
j

Joffrey

01/29/2023, 3:57 PM
@dave08 the thing you wrote here is not the compiler warning, that's the runtime error you get. The warning should be something like
Unchecked cast: SomeInterface? to T
. This means that even though you're writing an explicit cast here with
as?
, it will not check the type of the value at runtime, so the cast will never fail and the value will always be returned as-is. That is why the function happily returns a
Foo
even when called with
<Bar>
. The cast that fails is probably not the one you wrote with
as?
, but the one the compiler does automatically afterwards during the variable assignment. To fix it, you can use an inline function with
reified T
, so the cast is not unchecked:
private inline fun <reified T : SomeInterface> getOrNull(key: String): T? =
    actions[key] as? T
https://pl.kotl.in/LgsspJQUH
c

Chris Lee

01/29/2023, 3:58 PM
Yep. It works with reification:
inline fun <reified T : SomeInterface> getOrNull(key: String): T? =
        actions[key] as? T
d

dave08

01/29/2023, 3:59 PM
Yeah, but if you noticed in the question
actions
is private in the class... so an inline function won't work here...
j

Joffrey

01/29/2023, 3:59 PM
Then make it
@PublishedApi internal
c

Chris Lee

01/29/2023, 4:00 PM
or have a helper function to bridge the visibility gap.
d

dave08

01/29/2023, 4:00 PM
or have a helper function
how? I can't cast to a KClass...?
c

Chris Lee

01/29/2023, 4:01 PM
d

dave08

01/29/2023, 4:01 PM
Then make it
@PublishedApi internal
It's used in the same module..
Oh, that's good...
j

Joffrey

01/29/2023, 4:03 PM
If you don't want to make
actions
visible to the module, you can either provide an untyped helper as suggested by Chris, or make a typed helper that uses a
KClass
, and then a reified helper for convenience:
inline fun <reified T : SomeInterface> getOrNull(key: String): T? = getOrNull(key, T::class)

fun <T : SomeInterface> getOrNull(key: String, type: KClass<T>): T? =
    type.safeCast(actions[key])
d

dave08

01/29/2023, 4:06 PM
Does that
type.safeCast
require the reflection libs?
j

Joffrey

01/29/2023, 4:07 PM
Ah maybe, I just tried on the playground, but it's in
kotlin.reflect.full
so there is a chance, yes
Maybe Chris's option is better for your case, but I'm not sure it's much better than just exposing
actions
as a read-only map, to be frank 😄
d

dave08

01/29/2023, 4:09 PM
True in general, but here I'm abstracting away the fact that the key is a value class internally.
c

Chris Lee

01/29/2023, 4:10 PM
exposing the map as readonly also allows changing
getOrNull
to be an extension function for a more natural feel.
val actions = mutableMapOf<String, SomeInterface>()

interface SomeInterface {
    
}

class Foo : SomeInterface {
    
}

class Bar: SomeInterface {
    
}

inline fun <reified T : SomeInterface> Map<String,SomeInterface>.getOrNull(key: String): T? =
        actions[key] as? T

//public fun getAction(key : String) : SomeInterface? {
//    return actions[key]
//}

// I have a Foo in the "something" key so this should be null... Bar and Foo are both implementation of SomeInterface...


/**
 * You can edit, run, and share this code.
 * <http://play.kotlinlang.org|play.kotlinlang.org>
 */
fun main() {
    actions["foo"] = Foo()
    actions["bar"] = Bar()
	val tryToGetAFoo = actions.getOrNull<Bar>("foo")

}
True in general, but here I’m abstracting away the fact that the key is a value class internally.
Is it really abstracted away, though? Having to pass in a key to getOrNull exposes that there’s a key/value relationship.
d

dave08

01/29/2023, 4:14 PM
Yeah, but outside of this class I need to allow access w/o wrapping the key everywhere I need to access it's contents. If I'd be passing around the wrapped key (which might happen in the future), then I'd probably change that.
j

Joffrey

01/29/2023, 4:28 PM
What's the point of wrapping the key if not to make consumers type-safe?
I would have expected the opposite: internally you can use raw values, but for your public API you want better types for your consumers safety
d

dave08

01/29/2023, 4:31 PM
I'm refactoring... so I need to do things step by step, I don't want to break too much and have to rewrite all the unit tests at once...
j

Joffrey

01/29/2023, 4:32 PM
ah ok, it sounded like an actual design decision, not a temporary state for the refac. Then I understand 🙂