Map Scala 中的不同类型
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/17684023/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Different types in Map Scala
提问by pichsenmeister
I need a Map where I put different types of values (Double, String, Int,...) in it, key can be String.
我需要一个 Map,在其中放置不同类型的值(Double、String、Int...),键可以是 String。
Is there a way to do this, so that I get the correct type with map.apply(k)like
有没有办法做到这一点,以便我获得正确的类型,map.apply(k)例如
val map: Map[String, SomeType] = Map()
val d: Double = map.apply("double")
val str: String = map.apply("string")
I already tried it with a generic type
我已经用泛型类型试过了
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)
but it's not possible since Containertakes an parameter. Is there any solution to this?
但这是不可能的,因为Container需要一个参数。有什么解决办法吗?
回答by Rüdiger Klaehn
This is not straightforward.
这并不简单。
The type of the value depends on the key. So the key has to carry the information about what type its value is. This is a common pattern. It is used for example in SBT (see for example SettingsKey[T]) and Shapeless Records (Example). However, in SBT the keys are a huge, complex class hierarchy of its own, and the HList in shapeless is pretty complex and also does more than you want.
值的类型取决于键。所以密钥必须携带有关其值是什么类型的信息。这是一种常见的模式。例如,它用于 SBT(参见例如SettingsKey[T])和 Shapeless Records(示例)。然而,在 SBT 中,键本身就是一个巨大而复杂的类层次结构,而 shapeless 中的 HList 非常复杂,而且做的比你想要的更多。
So here is a small example of how you could implement this. The key knows the type, and the only way to create a Record or to get a value out of a Record is the key. We use a Map[Key, Any] internally as storage, but the casts are hidden and guaranteed to succeed. There is an operator to create records from keys, and an operator to merge records. I chose the operators so you can concatenate Records without having to use brackets.
所以这里有一个关于如何实现这一点的小例子。键知道类型,创建记录或从记录中获取值的唯一方法是键。我们在内部使用 Map[Key, Any] 作为存储,但转换是隐藏的并保证成功。有一个操作员可以从键创建记录,还有一个操作员可以合并记录。我选择了运算符,这样您就可以连接 Records 而不必使用括号。
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]
}
Here is how you would use this. First define some keys:
这是您将如何使用它。首先定义一些键:
val a = Key[Int]
val b = Key[String]
val c = Key[Float]
Then use them to create a record
然后用它们来创建记录
val record = a ~> 1 ++ b ~> "abc" ++ c ~> 1.0f
When accessing the record using the keys, you will get a value of the right type back
使用键访问记录时,您将返回正确类型的值
scala> record(a)
res0: Int = 1
scala> record(b)
res1: String = abc
scala> record(c)
res2: Float = 1.0
I find this sort of data structure very useful. Sometimes you need more flexibility than a case class provides, but you don't want to resort to something completely type-unsafe like a Map[String,Any]. This is a good middle ground.
我发现这种数据结构非常有用。有时您需要比 case 类提供的更大的灵活性,但您不想求助于像 Map[String,Any] 这样完全类型不安全的东西。这是一个很好的中间立场。
Edit: another option would be to have a map that uses a (name, type) pair as the real key internally. You have to provide both the name and the type when getting a value. If you choose the wrong type there is no entry. However this has a big potential for errors, like when you put in a byte and try to get out an int. So I think this is not a good idea.
编辑:另一种选择是拥有一个使用(名称,类型)对作为内部真实键的映射。获取值时,您必须同时提供名称和类型。如果您选择错误的类型,则没有条目。然而,这有很大的潜在错误,比如当你输入一个字节并试图取出一个 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)
}
Usage:
用法:
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)
回答by Miles Sabin
This is now very straightforward in shapeless,
这现在在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
This is with shapeless 2.0.0-SNAPSHOT as of the date of this answer.
截至本答案发布之日,这与 shapeless 2.0.0-SNAPSHOT 相同。
回答by pichsenmeister
I finally found my own solution, which worked best in my case:
我终于找到了自己的解决方案,在我的情况下效果最好:
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]
回答by Richard Sitze
(a) Scala containers don't track type information for what's placed inside them, and
(a) Scala 容器不跟踪放置在其中的内容的类型信息,并且
(b) the return "type" for an apply/get method with a simple Stringparameter/key is going to be static for a given instance of the object the method is to be applied to.
(b) 具有简单String参数/键的 apply/get 方法的返回“类型”对于要应用该方法的对象的给定实例将是静态的。
This feels very much like a design decision that needs to be rethought.
这感觉很像一个需要重新思考的设计决策。
回答by overthink
I don't think there's a way to get bare map.apply()to do what you'd want. As the other answers suggest, some sort of container class will be necessary. Here's an example that restricts the values to be only certain types (String, Double, Int, in this case):
我不认为有一种方法可以让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)
}
The main benefit of the sealted traitis compile-time checking for non-exhaustive matches in pattern matching.
的主要好处sealted trait是编译时检查模式匹配中的非穷举匹配。
I also like this approach because it's relatively simple by Scala standards. You can go off into the weeds for something more robust, but in my opinion you're into diminishing returns pretty quickly.
我也喜欢这种方法,因为按照 Scala 标准它相对简单。你可以为了更强大的东西而去杂草丛生,但在我看来,你很快就会陷入收益递减的境地。
回答by wheaties
There is a way but it's complicated. See Unboxed union types in Scala. Essentially you'll have to type the Mapto some type Int |v| Doubleto be able to hold both Intand Double. You'll also pay a high price in compile times.
有一种方法,但它很复杂。请参阅Scala 中的未装箱联合类型。本质上,您必须将 输入Map到某种类型Int |v| Double才能同时保存Int和Double。您还将在编译时间上付出高昂的代价。
回答by dhg
If you want to do this you'd have to specify the type of Containerto be Any, because Anyis a supertype of both Doubleand String.
如果你想这样做,你必须指定类型的Container是Any,因为Any是两者的超类型Double和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)
Or to make things easier, you can change the definition of Containerso that it's no longer type invariant:
或者为了使事情更容易,您可以更改 的定义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)

