类型推断与 Any-value Map 不一致

Type inference inconsistency with Any-value Map

在摆弄一个涉及使用任意类型值映射的片段时,如下所示,我遇到了一些类型推断不一致的问题:

import reflect.runtime.universe.TypeTag

case class AnyValMap[K]( m: Map[(K, TypeTag[_]), Any] ) extends AnyVal {
  def add[V](k: K, v: V)(implicit tag: TypeTag[V]) = this.copy(
      m = this.m + ((k, tag) -> v)
    )
  def grab[V](k: K)(implicit tag: TypeTag[V]): V = m((k, tag)).asInstanceOf[V]
}

val avMap = AnyValMap[String](Map.empty).
  add("a", 100).
  add("b", "xyz").
  add("c", 5.0).
  add("d", List(1, 2, 3))
// avMap: AnyValMap[String] = AnyValMap( Map(
//   (a,TypeTag[Int]) -> 100, (b,TypeTag[String]) -> xyz, (c,TypeTag[Double]) -> 5.0,
//   (d,TypeTag[List[Int]]) -> List(1, 2, 3)
// ) )

avMap.grab[Int]("a")
// res1: Int = 100

avMap.grab[String]("b")
// java.util.NoSuchElementException: key not found: (b,TypeTag[String]) ...

avMap.grab[Double]("c")
// res3: Double = 5.0

avMap.grab[List[Int]]("d")
// java.util.NoSuchElementException: key not found: (d,TypeTag[scala.List[Int]]) ...

现在,如果我在 add[V] 中使用显式类型信息组装 Map,一切都会正常进行:

val avMap = AnyValMap[String](Map.empty).
  add[Int]("a", 100).
  add[String]("b", "xyz").
  add[Double]("c", 5.0).
  add[List[Int]]("d", List(1, 2, 3))

avMap.grab[Int]("a")
// res5: Int = 100

avMap.grab[String]("b")
// res6: String = xyz

avMap.grab[Double]("c")
// res7: Double = 5.0

avMap.grab[List[Int]]("d")
// res8: List[Int] = List(1, 2, 3)

我的问题是:avMap 以前一种方式组装:

  1. avMap 似乎已经捕获了相应类型标签中的所有推断类型。为什么有些人查不到?
  2. 为什么在Int/DoubleString/List[T]之间查找类型的Map值不一致?也许,与 TypeTag 如何处理 AnyValAnyRef 下的类型有关?

我正在使用 Scala 2.11.12(而 2.12.x 似乎显示出同样的不一致)。提前致谢。

调用avMap.add(List(1, 2, 3))时推断出的类型是scala.collection.immutable.List[Int]。但是当您调用 avMap.grab[List[Int]] 时,将使用 scala.List[Int] 类型。此 scala.Listpackage object scala 中定义的别名。

Scala 理解这些类型是等价的:

scala> typeOf[scala.collection.immutable.List[Int]] =:= typeOf[scala.List[Int]]
res1: Boolean = true

但从TypeTag的角度来看,它们仍然是不同的类型,它们的类型和类型标签具有不同的哈希码,因此它们是 Map:[=32= 中的不同键]

scala> typeTag[scala.collection.immutable.List[Int]].hashCode()
res2: Int = 629297926

scala> typeTag[scala.List[Int]].hashCode()
res3: Int = 1684352762

同样的事情发生在 String 上,它是 scala.Predef 中定义的 java.lang.String 的别名。字符串常量的类型是java.lang.String,但是当你使用String类型作为类型参数时,它表示scala.Predef.String.

无论如何,将类型存储为键的方法非常脆弱。您还会遇到子类型的问题。例如,map.add(Some(10)) 推断类型为 Some[Int] 而不是预期的 Option[Int].


除非你真的想在同一个键下存储多个不同类型的值,否则我建议你将类型存储为值的一部分,并在检索时检查它是否符合请求的类型:

case class AnyValMap[K]( m: Map[K, (Type, Any)] ) extends AnyVal {
  def add[V](k: K, v: V)(implicit tag: TypeTag[V]) = this.copy(
    m = this.m + (k -> (tag.tpe, v))
  )
  def grab[V](k: K)(implicit tag: TypeTag[V]): V = {
    val (tpe, value) = m(k)
    if (tpe <:< tag.tpe) value.asInstanceOf[V]
    else throw new NoSuchElementException(s"wrong type $tpe of value for key: $k")
  }
}

这很好用,而且还允许通过它们的超类型获取值:

scala> val avMap = AnyValMap[String](Map.empty).add("f", Some("abc"))

scala> avMap.grab[AnyRef]("f")
res4: AnyRef = Some(abc)

scala> avMap.grab[Option[AnyRef]]("f")
res5: Option[AnyRef] = Some(abc)

scala> avMap.grab[Option[String]]("f")
res6: Option[String] = Some(abc)

scala> avMap.grab[Option[Int]]("f")
java.util.NoSuchElementException: wrong type scala.Some[java.lang.String] of value for key: f
  at AnyValMap$.grab$extension(<console>:28)
  ... 31 elided

如果您想要在同一个键下有多个不同类型的值,您能做的最好的可能是通过一个键的所有值的序列进行线性搜索:

case class AnyValMap[K]( m: Map[K, Vector[(Type, Any)]] ) extends AnyVal {
  def add[V](k: K, v: V)(implicit tag: TypeTag[V]) = this.copy(
    m = this.m + (k -> (this.m.getOrElse(k, Vector.empty) :+ (tag.tpe, v)))
  )
  def grab[V](k: K)(implicit tag: TypeTag[V]): V = {
    m(k).collectFirst {
      case (tpe, value: V @unchecked) if tpe <:< tag.tpe => value
    }.getOrElse(throw new NoSuchElementException(s"no suitable value for key: $k"))
  }
}

scala> val avMap = AnyValMap[String](Map.empty).
         add("a", List(1, 2, 3)).
         add("a", Some("abc"))

scala> avMap.grab[Seq[Int]]("a")
res20: Seq[Int] = List(1, 2, 3)

scala> avMap.grab[Option[String]]("a")
res21: Option[String] = Some(abc)

scala> avMap.grab[String]("a")
java.util.NoSuchElementException: no suitable value for key: a
  at AnyValMap$.$anonfun$grab$extension(<console>:32)
  at scala.Option.getOrElse(Option.scala:121)
  at AnyValMap$.grab$extension(<console>:32)
  ... 31 elided