使用任意值地图

时间:2018-03-29 20:53:09

标签: scala type-inference type-erasure

在摆弄涉及使用如下所示的Any-typed值的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]中使用显式类型信息汇编了地图,那么事情就可以了:

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]之间查找类型的地图值时出现不一致的情况?也许,这与TypeTag如何处理AnyValAnyRef下的类型?
  3. 有关

    我正在使用Scala 2.11.12(和2.12.x似乎表现出相同的不一致性)。提前谢谢。

1 个答案:

答案 0 :(得分:2)

致电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中的不同键:

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

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

同样的事情发生在String上,java.lang.Stringscala.Predef中定义的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$1(<console>:32)
  at scala.Option.getOrElse(Option.scala:121)
  at AnyValMap$.grab$extension(<console>:32)
  ... 31 elided

如果你希望在同一个键下有不同类型的多个值,那么你可以做的最好的事情就是对一个键的所有值的序列进行线性搜索:

{{1}}