可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I have a function that returns ( groovy code)
[words: "one two", row: 23, col: 45]
In scala I change above to scala Map but then I am forced to declare it as
Map[String, Any]
but this has the disadvantage that if I access a key such as
map("words")
I have to add the boilerplate
map2("words").asInstanceOf[String]
Is there a better way in scala that does not require me to add asInstanceOf?
回答1:
In order to avoid the casting and benefit from static-typing, you can either return a tuple (String, Int, Int)
:
def getResult = ("one two", 23, 45)
val res = getResult
res._1 // the line
// alternatively use the extractor
val (line, row, _) = getResult // col is discarded
line // the line
row // the row
or use a case class for the result:
case class MyResult(line: String, row: Int, col: Int)
def getResult = MyResult("one two", 23, 45)
val res = getResult
res.line // the line
// alternatively use the extractor provided by the case class
val MyResult(line, row, _) = getResult // col is discarded
line // the line
row // the row
I would prefer the case class because the fields are named and it's really just one line more.
回答2:
This is where case classes are your friend. If your map's keys are case classes, then you can force client code to correctly handle the types (and force it to correctly handle all of the types).
S:\>scala
Welcome to Scala version 2.9.0.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_25).
Type in expressions to have them evaluated.
Type :help for more information.
scala> sealed abstract class Thing;
defined class Thing
scala> case class Toaster(slices: Int) extends Thing;
defined class Toaster
scala> case class Bucket(contents: String) extends Thing;
defined class Bucket
scala> val things = Map("toasty" -> Toaster(2), "buck" -> Bucket("stuff"));
things: scala.collection.immutable.Map[java.lang.String,Product with Serializable with Thing] = Map(toasty -> Toaster(2), buck -> Bucket(stu
ff))
scala> for (x <- things) x match {
case (k,Toaster(s)) => println(k + " " + s)
case (k,Bucket(c)) => println(k + " " + c)
}
toasty 2
buck stuff
The key here is that the match statement is cracking out the various cases, and providing you with properly typed variables to match the fields within. By declaring that the abstract class as sealed, you are letting the compiler know that it has all of the available subclasses. With that information it can tell you when you have missing cases, and it could also do further optimization.
回答3:
Edit: I had understood that you got that map as is and needed a way to interface more cleanly with it. The case-class approach is, if applicable, superior to what I'm proposing below, of course.
If your keys are always mapped to the same value types, you could do something like this:
class TypedKey[T] {
def name = {
// assumes you declare only `object` instances
val simpleName = getClass.getSimpleName
val moduleName = if (simpleName.endsWith("$")) simpleName.substring(0, simpleName.size - 1) else simpleName
val lastDollar = moduleName.lastIndexOf('$')
if (lastDollar == -1) moduleName else moduleName.substring(lastDollar + 1)
}
}
object RubyKeys {
object words extends TypedKey[String]
object row extends TypedKey[Int]
object col extends TypedKey[Int]
}
class MapWrapper(val underlying: Map[String, Any]) {
def apply[T](key: TypedKey[T]) = underlying(key.name).asInstanceOf[T]
}
def main(args: Array[String]) {
val map = Map("words" -> "one two", "row" -> 23, "col" -> 45)
val wrapper = new MapWrapper(map)
import RubyKeys._
val w = wrapper(words) // String
val r = wrapper(row) // Int
val c = wrapper(col) // Int
println(w, r, c)
}
回答4:
Having only two possible value types as in your example would allow to use the Either
type with the sub-types Left
and Right
:
val m = Map("words" -> Left("one two"), "rows"-> Right(23), "cols"-> Right(45))
If you get back a value from the map, you can check what you have, e.g. with pattern matching or using isLeft
and isRight
, and "unwrap" it accordingly.