我需要一个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
采用参数,因此无法实现。这有什么解决方案吗?
答案 0 :(得分:22)
这不是直截了当的。
值的类型取决于键。所以关键是必须携带有关其价值的类型的信息。这是一种常见的模式。例如,它用于SBT(例如参见SettingsKey[T])和无形记录(Example)。但是,在SBT中,键是一个巨大的,复杂的类层次结构,而无形的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)
答案 1 :(得分:17)
现在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。
答案 2 :(得分:3)
(a)Scala容器不会跟踪放置在其中的内容的类型信息,
(b)对于要应用该方法的对象的给定实例,具有简单String
参数/键的apply / get方法的返回“type”将是静态的。
这非常像一个需要重新思考的设计决策。
答案 3 :(得分: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 :(得分:2)
我认为没有办法让你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标准相对简单。你可以去杂草寻找更强大的东西,但在我看来,你很快就会收益递减。
答案 5 :(得分:1)
如果你想这样做,你必须指定Container
的类型为Any
,因为Any
是Double
和{{的超类型1}}。
String
或者为了方便起见,您可以更改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
答案 6 :(得分:1)
有一种方法,但它很复杂。见Unboxed union types in Scala。基本上,您必须将Map
键入某种类型Int |v| Double
,才能同时拥有Int
和Double
。您还需要在编译时付出高昂的代价。