开发者

Inheritance (class hierarchy) of Equals and Ordered[T] (type parameter erasure)

开发者 https://www.devze.com 2023-04-03 10:16 出处:网络
I want to have several data classes which all have an identifier that is appointed to be sufficient for checking object equality.

I want to have several data classes which all have an identifier that is appointed to be sufficient for checking object equality. But this id: I should be used not only for test of equality but for comparison purposes too. That's why a view bound I <% Ordered[I] is claimed in following base class.

abstract class Ident[I <% Ordered[I]](val id: I)
  extends Equals with Ordered[Ident[I]]
{
  override lazy val hashCode = id.hashCode
  /* canEqual does not work as desired! */
  def canEqual(other: Any) = other match {
    case that: Ident[I] => true /* id < that.id || id > that.id || id == that.id */
    case _              => false
  }
  override def equals(other: Any) = other match {
    case that: Ident[I] => (that canEqual this) && this.id == that.id
    case _              => false
  }
  def _compare(that: Ident[I]): Int = {
    if (that canEqual this) this.id compare that.id else {
      val message = "'%s' and '%s' are not comparable!" format (this, that)
      throw new开发者_StackOverflow中文版 IllegalArgumentException(message)
    }
  }
  def compare(that: Ident[I]): Int = _compare(that)
}

The compare method is defined only when canEqual is true.

Because derived classes T <: Ident[I] should be Ordered[T] too, an implicit conversion is defined:

object Ident {
  implicit def ident2ordered[I, T <: Ident[I]](other: T): Ordered[T] = {
    new Ordered[T] {
      def compare(that: T): Int = other._compare(that)
    }
  }
}

And here are some derived data classes:

/* should be comparable with all other <: Ident[Int] */
class IntId(i: Int) extends Ident[Int](i)
/* should be comparable only with itself */
class YetAnotherIntId(y: Int) extends Ident[Int](y) {
  override def canEqual(other: Any) = other.isInstanceOf[YetAnotherIntId]
  override def equals(other: Any) = other match {
    case that: YetAnotherIntId => super.equals(that)
    case _                     => false
  }
}
/* should be comparable with all other <: Ident[Long] */
class LongId(j: Long) extends Ident[Long](j)
/* should be comparable with all other <: Ident[String] */
class StringId(s: String) extends Ident[String](s)

And now the noticed (but not always desired) behaviour:

val i12 = new IntId(12)
val i13 = new IntId(13)
i12 canEqual i13 /* => true */
i12 < i13        /* => true */

val y12 = new YetAnotherIntId(12)
val y13 = new YetAnotherIntId(13)
y12 canEqual y13 /* => true */
y12 < y13        /* => true */

i12 canEqual y12 /* => true */
y12 canEqual i12 /* => false */
i12 == y12       /* => false */
y12 == i12       /* => false */

val j12 = new LongId(12L)
val j13 = new LongId(13L)
j12 canEqual j13 /* => true */
j12 < j13        /* => true */

i12 canEqual j12 /* => true  but want false because Int != Long */
j12 canEqual i12 /* => true  '' */
i12 == j12       /* => true  '' */
j12 == i12       /* => true  '' */

val s12 = new StringId("12")
val s13 = new StringId("13")
s12 canEqual s13 /* => true */
s12 < s13        /* => true */

i12 canEqual s12 /* => true  but want false because Int != String */
s12 canEqual i12 /* => true  '' */
i12 == s12       /* => false */
s12 == i12       /* => false */

Thank you, if you've read this far, but now the questions:

How can I achieve that Ident[I].canEqual(Ident[J]) is false for I != J without overriding canEqual like in YetAnotherIntId?

It seems that an Ident[I] is an Ident[J] is an Ident[_] which would yield to problems when using this.id and that.id together by commenting out in Ident::canEqual (replacing true with this.id < that.id || this.id > that.id || this.id == that.id).

So, why Ident[Int].canEqual(Ident[Long]) is true? Due to type erasure? Is it possible to "repair" this whith an Manifest? Or is there another possibility to ensure that I == J?


Type erasure indeed. You must have got some warning on your case that: Ident[I]. It checks you have an Ident[somehing] but not that this something is I (same as in java). It is better to check with that: Ident[_] where you have no false assurance.

You might indeed have a Manifest in your class, and check that the manifests are equals.

class Ident[I <% Ordered](val id: I)(implicit val manifest: Manifest[I])
  def canEqual(that: Any) = that match {
    case Ident[_] if this.manifest == that.manifest => ...
    case _ => false
  }
}

I would suggest I : Ordering rather than I % Ordered (in general, not just for your problem). You could then compare the orderings rather than the manifests. Also you should use that to do the equal on the ids too, this way you know it is consistent with ordering

Edit Regarding your comment.

I would think this and that having the same Ordering is enough to say canEqual is true. Then, you have this.Odering which the compiler known is an Ordering[I], and that.Ordering which is an ordering on a type the compiler does not known. You will have to use the first one, which will imply an unchecked cast (pattern matching or otherwise) of that.id to I. However, having checked the ordering equality should be enough to ensure the cast is safe (Ordering is invariant)


Regarding didierd's answer tried to use an implicit ord: Ordering[I] to replace the otherwise necessary Manifest for ensuring type compatibility. This works fine and it came out the following.

abstract class Ident[I](val id: I, dom: String = "")(implicit val ord: Ordering[I])
  extends Equals with Ordered[Ident[I]]
{
  protected val domain: AnyRef = if (dom != "") dom else ord

  override lazy val hashCode = id.##
  def canEqual(other: Any) = other match {
    case that: Ident[_] => this.domain eq that.domain
    case _              => false
  }
  override def equals(other: Any) = other match {
    /* ugly: non variable type-argument I in type pattern Ident[I]
       is unchecked since it is eliminated by erasure */
    case that: Ident[I] if this.domain eq that.domain
           => (that canEqual this) && ord.compare(this.id, that.id) == 0
    case _ => false
  }
  def compare(that: Ident[I]): Int = if (that canEqual this)
    ord compare (this.id, that.id)
  else
  {
    val message = "'%s' and '%s' are not comparable!" format (this, that)
    throw new IllegalArgumentException(message)
  }
  lazy val nameId: String = getClass.getSimpleName + "#" + id
  override def toString = nameId
}

It is possible to create domains (of sub class hierarchies) whose elements are comparable among each other

/* Order, SubOrder and all other <: Ident[Int] without explicit dom are comparable */
class Order(i: Int) extends Ident[Int](i)
class SubOrder(i: Int) extends Order(i)

/* Node and SubNode's are in its own "Node" named domain
   and comparable only among each other */
class Node(i: Int) extends Ident[Int](i, "Node")
class SubNode(i: Int) extends Node(i)

without implementing separate canEqual boilerplate for each domain.

Only one thing remain: the ugly warning Ident's equals method.

0

精彩评论

暂无评论...
验证码 换一张
取 消