Generics in a bidirectional association
Let's say I have two classes A and B, with B a subtype of A. This is only part of a richer type hierarchy, obviously, but I don't think that's relevant. Assume A is the root of the hierarchy. There is a collection class C that keeps track of a list of A's. However, I want to make C generic, so that it is possible to make an instance that only keeps B's and won't accept A's.
class A(val c: C[A]) {
c.addEntry(this)
}
class B(c: C[A]) extends A(c)
class C[T <: A]{
val entries = new ArrayBuffer[T]()
def addEntry(e: T) { entries += e }
}
object Generic {
def main(args : Array[String]) {
val c = new C[B]()
new B(c)
}
}
The code above obviously give the error 'type mismatch: found C[B], required C[A]' on the new B(c)
line.
I'm not sure how this can be fixed. It's not possible to make C covariant in T (like C[+T <: A]
) because the ArrayBuffer is non-variantly typed in T. It's not possible to make the constructor of B require a C[B] because C can't be covariant.
Am I barking up the wrong tree here? I'm a complete Scala newbie, so any ideas and tips might be helpful. Thank you!
EDIT: Basically, what I'd like to have is that the compiler accepts both
val c = new C[B]()
new B(c)
and
val c = new C[A]()
new B(c)
but would reject
val c = new C[B]()
new A(c)
It's probably possible to relax the typing of the ArrayBuffer in C to be A instead of T, and thus in 开发者_运维技巧the addEntry method as well, if that helps.
It's not possible to make C covariant in T (like
C[+T <: A]
) because the ArrayBuffer is non-variantly typed in T
Not only because of this. The type of addEntry
is enough to disallow it:
val a: A = ...
val b: B = ...
val cb: C[B] = ...
cb.addEntry(b) // works
cb.addEntry(a) // doesn't and shouldn't
Hacky, but seems to work:
class A(val c: C[A]) {
c.addEntry(this.asInstanceOf[c.X])
}
class B(c: C[B]) extends A(c)
class C[+T <: A] {
type X <: T
val entries = new ArrayBuffer[X]()
def addEntry(e: X) { entries += e }
}
object Generic {
def main(args : Array[String]) {
val c = new C(){ type T = B }
new B(c)
}
}
Of course I would be interested in a proper solution as well...
If you want to keep track of the instances of A, you have to pass an instance of C[A] to the constructor of B since every B is also an A:
def main(args : Array[String]) {
val c = new C[A]()
new B(c)
}
If however you want to keep track of the Bs then you can't delegate this down to A since A doesn't know anything about B.
Overall, I have the feeling your problem is somewhat ill posed.
Let's say it was possible. Then you'd be able to do this:
class A(val c: C[A]) {
c.addEntry(this)
}
class B(c: C[A]) extends A(c)
class C[+T <: A]{
val entries: ArrayBuffer[T] @uncheckedVariance = new ArrayBuffer[T]()
def addEntry(e: T @uncheckedVariance) { entries += e }
}
object Generic {
def main(args : Array[String]) {
// Everything's fine so far...
val c = new C[B]()
c.addEntry(new B(c))
// but, suddenly...
val ca: C[A] = c
ca.addEntry(new A(ca))
// a problem appears!
c.entries forall {
case thing: B => true // ok
case otherThing => false // not ok -- c now contains an A!
}
}
}
Trying to run this code will result in a class cast exception.
Edit
You added this requirement:
val c = new C[B]() new B(c)
and
val c = new C[A]() new B(c)
but would reject
val c = new C[B]() new A(c)
However, if B
is initialized with C[B]
, and given that B
extends A
, then B
will initialize A
with C[B]
, thereby violating the last requirement.
精彩评论