【问题标题】:Different types in Map ScalaMap Scala 中的不同类型
【发布时间】:2013-07-15 01:48:45
【问题描述】:

我需要一个 Map,在其中放置不同类型的值(Double、String、Int、...),key 可以是 String。

有没有办法做到这一点,这样我就可以用map.apply(k) 得到正确的类型

val map: Map[String, SomeType] = Map()
val d: Double = map.apply("double")
val str: String = map.apply("string")

我已经用泛型尝试过了

class Container[T](element: T) {
    def get: T = element
}

val d: Container[Double] = new Container(4.0)
val str: Container[String] = new Container("string")
val m: Map[String, Container] = Map("double" -> d, "string" -> str)

但这是不可能的,因为Container 需要一个参数。有什么解决办法吗?

【问题讨论】:

标签: scala generics shapeless


【解决方案1】:

这并不简单。

值的类型取决于键。所以键必须携带关于它的值是什么类型的信息。这是一种常见的模式。例如,它用于 SBT(参见例如 SettingsKey[T])和 Shapeless Records (Example)。然而,在 SBT 中,键本身就是一个巨大而复杂的类层次结构,而 shapeless 中的 HList 相当复杂,而且功能也超出了您的预期。

所以这里有一个小例子来说明如何实现这一点。键知道类型,创建记录或从记录中获取值的唯一方法是键。我们在内部使用 Map[Key, Any] 作为存储,但转换是隐藏的并保证成功。有一个运算符用于从键创建记录,还有一个运算符用于合并记录。我选择了运算符,这样您就可以连接记录而不必使用括号。

sealed trait Record {

  def apply[T](key:Key[T]) : T

  def get[T](key:Key[T]) : Option[T]

  def ++ (that:Record) : Record
}

private class RecordImpl(private val inner:Map[Key[_], Any]) extends Record {

  def apply[T](key:Key[T]) : T = inner.apply(key).asInstanceOf[T]

  def get[T](key:Key[T]) : Option[T] = inner.get(key).asInstanceOf[Option[T]]

  def ++ (that:Record) = that match {
    case that:RecordImpl => new RecordImpl(this.inner ++ that.inner)
  }
}

final class Key[T] {
  def ~>(value:T) : Record = new RecordImpl(Map(this -> value))
}

object Key {

  def apply[T] = new Key[T]
}

以下是您将如何使用它。首先定义一些键:

val a = Key[Int]
val b = Key[String]
val c = Key[Float]

然后用它们来创建记录

val record = a ~> 1 ++ b ~> "abc" ++ c ~> 1.0f

当使用键访问记录时,你会得到一个正确类型的值

scala> record(a)
res0: Int = 1

scala> record(b)
res1: String = abc

scala> record(c)
res2: Float = 1.0

我发现这种数据结构非常有用。有时您需要比案例类提供的更大的灵活性,但您不想求助于完全类型不安全的东西,例如 Map[String,Any]。这是一个很好的中间立场。


编辑:另一种选择是在内部使用(名称,类型)对作为真正的键的映射。获取值时必须提供名称和类型。如果您选择了错误的类型,则没有条目。然而,这有很大的潜在错误,比如当你输入一个字节并试图取出一个 int 时。所以我认为这不是一个好主意。

import reflect.runtime.universe.TypeTag

class TypedMap[K](val inner:Map[(K, TypeTag[_]), Any]) extends AnyVal {
  def updated[V](key:K, value:V)(implicit tag:TypeTag[V]) = new TypedMap[K](inner + ((key, tag) -> value))

  def apply[V](key:K)(implicit tag:TypeTag[V]) = inner.apply((key, tag)).asInstanceOf[V]

  def get[V](key:K)(implicit tag:TypeTag[V]) = inner.get((key, tag)).asInstanceOf[Option[V]]
}

object TypedMap {
  def empty[K] = new TypedMap[K](Map.empty)
}

用法:

scala> val x = TypedMap.empty[String].updated("a", 1).updated("b", "a string")
x: TypedMap[String] = TypedMap@30e1a76d

scala> x.apply[Int]("a")
res0: Int = 1

scala> x.apply[String]("b")
res1: String = a string

// this is what happens when you try to get something out with the wrong type.
scala> x.apply[Int]("b")
java.util.NoSuchElementException: key not found: (b,Int)

【讨论】:

  • 这是一个不错的模式。不幸的是,您必须对 apply 方法进行类型注释。无论如何,我认为这是迄今为止最好的解决方案。谢谢你的回答!
  • 你是说第二个?请注意,通过使用 ClassTag,当您放入没有 JVM 等效项的 scala 类型时,您会遇到问题。例如,您可以输入一个 List[Int] 并请求一个 List[String],您会得到一些东西,因为 List[Int] 和 List[String] 确实具有相同的擦除。如果您希望它也能在这种情况下工作,您将不得不使用 TypeTag,它捕获整个 scala 类型,而不仅仅是 JVM 类型。我相应地更新了示例。
  • 如果你不需要 shapeless 的所有通用魔法,但仍然需要具有多种类型值的容器,你的第一个模式是一个非常有用的答案,非常感谢!
【解决方案2】:

这在shapeless 中现在非常简单,

scala> import shapeless._ ; import syntax.singleton._ ; import record._
import shapeless._
import syntax.singleton._
import record._

scala> val map = ("double" ->> 4.0) :: ("string" ->> "foo") :: HNil
map: ... <complex type elided> ... = 4.0 :: foo :: HNil

scala> map("double")
res0: Double with shapeless.record.KeyTag[String("double")] = 4.0

scala> map("string")
res1: String with shapeless.record.KeyTag[String("string")] = foo

scala> map("double")+1.0
res2: Double = 5.0

scala> val map2 = map.updateWith("double")(_+1.0)
map2: ... <complex type elided> ... = 5.0 :: foo :: HNil

scala> map2("double")
res3: Double = 5.0

截至本答案发布之日,这是无形的 2.0.0-SNAPSHOT。

【讨论】:

  • 这很整洁。但是当使用 HList 作为映射时,访问特性是什么?因为它基本上是一个单链表,你必须检查每个元素,所以查找将是 O(N),对吧?此外,一旦您拥有多个元素,这不会创建非常复杂的类型吗?
【解决方案3】:

我终于找到了自己的解决方案,在我的情况下效果最好:

case class Container[+T](element: T) {
    def get[T]: T = {
        element.asInstanceOf[T]
    }
}

val map: Map[String, Container[Any]] = Map("a" -> Container[Double](4.0), "b" -> Container[String]("test"))
val double: Double = map.apply("a").get[Double]
val string: String = map.apply("b").get[String]

【讨论】:

    【解决方案4】:

    (a) Scala 容器不跟踪放置在其中的内容的类型信息,并且

    (b) 带有简单String 参数/键的应用/获取方法的返回“类型”对于要应用该方法的对象的给定实例将是静态的。

    这感觉就像一个需要重新考虑的设计决策。

    【讨论】:

      【解决方案5】:

      我认为没有办法让map.apply() 做你想做的事。正如其他答案所暗示的那样,某种容器类将是必要的。这是一个将值限制为仅某些类型(在本例中为 String、Double、Int)的示例:

      sealed trait MapVal
      case class StringMapVal(value: String) extends MapVal
      case class DoubleMapVal(value: Double) extends MapVal
      case class IntMapVal(value: Int) extends MapVal
      
      val myMap: Map[String, MapVal] =                                                               
        Map("key1" -> StringMapVal("value1"),
            "key2" -> DoubleMapVal(3.14),
            "key3" -> IntMapVal(42))
      
      myMap.keys.foreach { k =>
        val message =
          myMap(k) match { // map.apply() in your example code
            case StringMapVal(x) => "string: %s".format(x)
            case DoubleMapVal(x) => "double: %.2f".format(x)
            case IntMapVal(x) => "int: %d".format(x)
          }
        println(message)
      }
      

      sealted trait 的主要好处是在编译时检查模式匹配中的非详尽匹配。

      我也喜欢这种方法,因为按照 Scala 标准它相对简单。你可以去寻找更强大的东西,但在我看来,你很快就会陷入收益递减。

      【讨论】:

        【解决方案6】:

        如果您想这样做,您必须将Container 的类型指定为Any,因为AnyDoubleString 的超类型。

        val d: Container[Any] = new Container(4.0)
        val str: Container[Any] = new Container("string")
        val m: Map[String, Container[Any]] = Map("double" -> d, "string" -> str)
        

        或者为了让事情更简单,您可以更改Container 的定义,使其不再是类型不变的:

        class Container[+T](element: T) {
          def get: T = element
          override def toString = s"Container($element)"
        }
        
        val d: Container[Double] = new Container(4.0)
        val str: Container[String] = new Container("string")
        val m: Map[String, Container[Any]] = Map("double" -> d, "string" -> str)
        

        【讨论】:

        • 我已经试过了。但是如果我将Cointainer 指定为Any,我会在map.apply(k) 上得到Any 值,例如val d: Double = map.apply("double") 是不可能的。
        【解决方案7】:

        有一种方法,但它很复杂。见Unboxed union types in Scala。本质上,您必须将Map 键入某种Int |v| Double 才能同时保存IntDouble。您还将在编译时间上付出高昂的代价。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-06-03
          • 1970-01-01
          • 2016-09-11
          • 2017-07-14
          • 2019-03-15
          • 1970-01-01
          • 2013-06-20
          相关资源
          最近更新 更多