Reputation: 83
I stuck with some simple thing) Let's say I have following:
interface IMessagePayload // marker interface
data class IdPayload(
val id: Long
) : IMessagePayload
data class StringPayload(
val id: String,
) : IMessagePayload
Then I have a class:
data class Message<T : IMessagePayload>(
val id: String,
val payload: T,
)
Also I have some interface describing processor of this message:
interface IMessageProcessor<T : IMessagePayload> {
fun process(message: Message<T>)
}
And some implementation:
class ProcessorImpl : IMessageProcessor<IdPayload> {
override fun process(message: Message<IdPayload>) {
}
}
Now I wanna have a map of such processors. Lets use some enum type as a keys of this map:
enum class ActionType {
UPDATE,
DELETE,
ADD
}
private var map = mutableMapOf<ActionType, IMessageProcessor<IMessagePayload>>()
map[ActionType.ADD] = ProcessorImpl() // <-- error here
And that's where the problem occurs. I cannot put my ProcessorImpl into this map. The compiler says that there is an error: Type mismatch. Required: IMessageProcessor. Found: ProcessorImpl().
I could declare the map in the following way (using star projection):
private var map = mutableMapOf<ActionType, IMessageProcessor<*>>()
But in this case I cannot call processors's process method fetching it from the map by key first:
map[ActionType.ADD]?.process(Message("message-id", IdPayload(1))) // <-- error here
Compiler complains: Type mismatch. Required nothing. Found Message<IdPayload>
What am I doing wrong? Any help is appreciated.
Upvotes: 0
Views: 1559
Reputation: 18617
This is about variance.
IMessageProcessor
is defined as interface IMessageProcessor<T : IMessagePayload>
; it has one type parameter, which must be IMessagePayload
or a subtype.
But it is invariant in that type parameter; an IMessageProcessor< IdPayload>
is not related to an IMessageProcessor<IMessagePayload>
. In particular, it's not a subtype.
And your map
is defined with a value type IMessageProcessor<IMessagePayload>
. So its value cannot be an IMessageProcessor< IdPayload>
, because that's neither the value type, nor a subtype. Hence the compile error.
In this case, the simplest way to get it to compile is to change your map
:
private var map = mutableMapOf<ActionType, IMessageProcessor<out IMessagePayload>>()
The only difference there is the out
; that tells the compiler that the value IMessageProcessor
is covariant in its type parameter. (It may help to think of out
as meaning ‘…or any subtype’. Similarly, you could make it contravariant by using in
, which you might think of as ‘…or any supertype’.)
This lets you store in the map an IMessageProcessor
for any subtype of IMessagePayload
.
However, if you do that, you'll find that you can't use any value you pull out of your map — because it can't tell which messages the processor can handle, i.e. which subtype of IMessagePayload
it works for! (The compiler expresses this as expecting a type parameter of Nothing
.)
In general, it's often better to specify variance on the interface or superclass itself (declaration-site variance) rather than the use-site variance shown above. But I can't see a good way to do that here, because you have multiple generic classes, and they interact in a complicated way…)
Think for a moment what IMessageProcessor
's type parameter means: it's the type of message that the processor can consume. So an IMessageProcessor<A>
can handle messages of type Message<A>
.
Now, a subtype must be able to do everything its supertype can do (and usually more) — otherwise you can't drop that subtype anywhere that's expecting to use the supertype. (That has the grand name of the Liskov substitution principle — but it's really just common sense.)
So an IMessageProcessor<B>
is a subtype of IMessageProcessor<A>
only if it can handle at least all the messages that an IMessageProcessor<A>
can. This means it must accept all messages of type Message<A>
.
But Message
is invariant in its type parameter: a Message<B>
is not directly related to a Message<A>
. So you can't write a processor that handles them both.
The most natural solution I can find is to specify variance on both Message
and IMessageProcessor
:
data class Message<out T : IMessagePayload>( /*…*/ )
interface IMessageProcessor<in T : IMessagePayload> { /*…*/ }
And then use a wildcard in your map to make it explicit that you don't know anything about the type parameters of its values:
private var map = mutableMapOf<ActionType, IMessageProcessor<*>>()
That lets you safely store a ProcessorImpl()
in the map.
But you still have to use an (unchecked) cast on the values you pull out of the map before you can use them:
(map[ActionType.ADD] as IMessageProcessor<IdPayload>)
.process(Message("4", IdPayload(4L)))
I don't think there's any easy way around that, because the problem is inherent in having values which are processors that can handle only some (unknown) types of message.
I'm afraid the best thing would be to have a rethink about what these classes mean and how they should interact, and redesign accordingly.
Upvotes: 3