I am thinking of the following example to illustrate why contravariance is useful.
Let's consider a GUI framework with Widgets
, Events
, and Event Listeners
.
abstract class Event;
class KeyEvent extends Event
class MouseEvent extends Event
trait EventListener[-E] { def listen(e:E) }
Let Widgets
define the following methods:
def addKeyEventListener(listener:EventListener[KeyEvent])
def addMouseEventListener(listener:EventListener[MouseEvent])
These methods a开发者_如何学Cccept only "specific" event listeners, which is fine. However I would like to define also "kitchen-sink" listeners, which listen to all events, and pass such listeners to the "add listener" methods above.
For instance, I would like to define LogEventListener
to log all incoming events
class LogEventListener extends EventListener[Event] {
def listen(e:Event) { log(event) }
}
Since the trait EventListener
is contravariant in Event
we can pass LogEventListener
to all those "add listener" methods without losing their type safety.
Does it make sense ?
It makes sense to me, anyway. And it is also one of the most intuitive examples I have seen: something which listens to all events naturally will listen to key events or mouse events.
Makes sense to me too. As a rule of thumb, a parametrized type Type[A]
should be contravariant with respect to its type parameter A
each time it is meant to accept instances of A
to do something with them by means of accepting them as parameters.
For instance, the Java type Comparator[T]
, if it had been defined in Scala, would have been contravariant: a Comparator[Any]
should be a subtype of Comparator[String]
, as it can compare all objects a Comparator[String]
can compare, and more. The most general example is the argument types of the FunctionX
classes, which are all contravariant.
精彩评论