假设我有这个示例类:
case class Test(key1: Int, key2: String, key3: String)
而我拥有一张地图
myMap = Map("k1" -> 1, "k2" -> "val2", "k3" -> "val3")
我需要在代码的多个位置将此地图(map)转换为我的案例类(case class),就像这样:
myMap.asInstanceOf[Test]
最简单的方法是什么?我能以某种方式使用implicit来完成吗?
假设我有这个示例类:
case class Test(key1: Int, key2: String, key3: String)
而我拥有一张地图
myMap = Map("k1" -> 1, "k2" -> "val2", "k3" -> "val3")
我需要在代码的多个位置将此地图(map)转换为我的案例类(case class),就像这样:
myMap.asInstanceOf[Test]
最简单的方法是什么?我能以某种方式使用implicit来完成吗?
有两种优雅的方法可以实现这个。第一种是使用 unapply
,第二种是使用隐式类(2.10+)和类型类来为你进行转换。
1) 使用 unapply
是编写这种转换最简单、最直接的方法。它不会进行任何"魔法"操作,并且如果使用 IDE 工具,可以轻松地找到代码。需要注意的是,这样做可能会使你的伴生对象变得混乱,并且导致代码在你不希望的地方产生依赖:
object MyClass{
def unapply(values: Map[String,String]) = try{
Some(MyClass(values("key").toInteger, values("next").toFloat))
} catch{
case NonFatal(ex) => None
}
}
可以像这样使用:
val MyClass(myInstance) = myMap
如果没有完全匹配,要小心,否则会抛出异常。
2)使用隐式类和类型类创建更多样板文件,但也允许将相同的模式扩展到其他情况类中:
implicit class Map2Class(values: Map[String,String]){
def convert[A](implicit mapper: MapConvert[A]) = mapper conv (values)
}
trait MapConvert[A]{
def conv(values: Map[String,String]): A
}
举个例子,您可以这样做:
object MyObject{
implicit val new MapConvert[MyObject]{
def conv(values: Map[String, String]) = MyObject(values("key").toInt, values("foo").toFloat)
}
}
那么它可以像你上面描述的那样使用:
val myInstance = myMap.convert[MyObject]
如果无法进行转换,抛出异常。使用这种模式将 Map[String, String]
与任何对象之间的转换只需要另一个隐式转换(且该隐式转换处于作用域内)。
object MyObject { ... }
无法编译通过。 - user3685285MyClass
是一个带有伴生对象的 case 类,那么你的 unapply
方法是否需要使用不同的名称来表示该对象 因为默认情况下已经定义了 unapply
方法?例如,我只能编译出类似这样的代码:val MyClassOfMap(myInstance) = myMap
。 - ecoeunapply
而不是apply
。Scala文档说“apply方法就像一个构造函数,它接受参数并创建对象,而unapply接受一个对象并尝试返回参数。”。既然我们正在将参数(Map)转换为对象(MyClass),那么应该使用apply
吗? - greenTea这里有一种替代的非样板方法,它使用Scala反射(Scala 2.10及以上版本),不需要单独编译模块:
import org.specs2.mutable.Specification
import scala.reflect._
import scala.reflect.runtime.universe._
case class Test(t: String, ot: Option[String])
package object ccFromMap {
def fromMap[T: TypeTag: ClassTag](m: Map[String,_]) = {
val rm = runtimeMirror(classTag[T].runtimeClass.getClassLoader)
val classTest = typeOf[T].typeSymbol.asClass
val classMirror = rm.reflectClass(classTest)
val constructor = typeOf[T].decl(termNames.CONSTRUCTOR).asMethod
val constructorMirror = classMirror.reflectConstructor(constructor)
val constructorArgs = constructor.paramLists.flatten.map( (param: Symbol) => {
val paramName = param.name.toString
if(param.typeSignature <:< typeOf[Option[Any]])
m.get(paramName)
else
m.get(paramName).getOrElse(throw new IllegalArgumentException("Map is missing required parameter named " + paramName))
})
constructorMirror(constructorArgs:_*).asInstanceOf[T]
}
}
class CaseClassFromMapSpec extends Specification {
"case class" should {
"be constructable from a Map" in {
import ccFromMap._
fromMap[Test](Map("t" -> "test", "ot" -> "test2")) === Test("test", Some("test2"))
fromMap[Test](Map("t" -> "test")) === Test("test", None)
}
}
}
import scala.reflect.macros.Context
trait Mappable[T] {
def toMap(t: T): Map[String, Any]
def fromMap(map: Map[String, Any]): T
}
object Mappable {
implicit def materializeMappable[T]: Mappable[T] = macro materializeMappableImpl[T]
def materializeMappableImpl[T: c.WeakTypeTag](c: Context): c.Expr[Mappable[T]] = {
import c.universe._
val tpe = weakTypeOf[T]
val companion = tpe.typeSymbol.companionSymbol
val fields = tpe.declarations.collectFirst {
case m: MethodSymbol if m.isPrimaryConstructor ⇒ m
}.get.paramss.head
val (toMapParams, fromMapParams) = fields.map { field ⇒
val name = field.name
val decoded = name.decoded
val returnType = tpe.declaration(name).typeSignature
(q"$decoded → t.$name", q"map($decoded).asInstanceOf[$returnType]")
}.unzip
c.Expr[Mappable[T]] { q"""
new Mappable[$tpe] {
def toMap(t: $tpe): Map[String, Any] = Map(..$toMapParams)
def fromMap(map: Map[String, Any]): $tpe = $companion(..$fromMapParams)
}
""" }
}
}
如果你在 Scala 中使用 jackson,这对我来说很有效:
def from[T](map: Map[String, Any])(implicit m: Manifest[T]): T = {
val mapper = new ObjectMapper() with ScalaObjectMapper
mapper.convertValue(map)
}
我不太喜欢这段代码,但是如果你可以将地图的值转换为元组,然后使用tupled
构造器来创建你的案例类,那么这种方法也是可行的。代码大致如下:
val myMap = Map("k1" -> 1, "k2" -> "val2", "k3" -> "val3")
val params = Some(myMap.map(_._2).toList).flatMap{
case List(a:Int,b:String,c:String) => Some((a,b,c))
case other => None
}
val myCaseClass = params.map(Test.tupled(_))
println(myCaseClass)
您需要小心确保值列表恰好有3个元素且类型正确。否则,您将得到一个None。就像我说的那样,这不是很好,但它表明这是可能的。
myMap.map(_._2)
替换为myMap.values
。 - Mestre Sanimport scala.reflect.ClassTag
object Reflect:
def fromMap[T <: Product : ClassTag](m: Map[String, ?]): T =
val classTag = implicitly[ClassTag[T]]
val constructor = classTag.runtimeClass.getDeclaredConstructors.head
val constructorArgs = constructor.getParameters()
.map { param =>
val paramName = param.getName
if (param.getType == classOf[Option[_]])
m.get(paramName)
else
m.get(paramName)
.getOrElse(throw new IllegalArgumentException(s"Missing required parameter: $paramName"))
}
constructor.newInstance(constructorArgs: _*).asInstanceOf[T]
以下是上述内容的测试:
case class Foo(a: String, b: Int, c: Option[String] = None)
case class Bar(a: String, b: Int, c: Option[Foo])
class ReflectSuite extends munit.FunSuite:
test("fromMap") {
val m = Map("a" -> "hello", "b" -> 42, "c" -> "world")
val foo = Reflect.fromMap[Foo](m)
assertEquals(foo, Foo("hello", 42, Some("world")))
val n = Map("a" -> "hello", "b" -> 43)
val foo2 = Reflect.fromMap[Foo](n)
assertEquals(foo2, Foo("hello", 43))
val o = Map("a" -> "yo", "b" -> 44, "c" -> foo)
val bar = Reflect.fromMap[Bar](o)
assertEquals(bar, Bar("yo", 44, Some(foo)))
}
test("fromMap should fail when required parameter is missing") {
val m = Map("a" -> "hello", "c" -> "world")
intercept[java.lang.IllegalArgumentException] {
Reflect.fromMap[Foo](m)
}
}
commons.mapper.Mappers.mapToBean[CaseClassBean](map)
def map2Test(m: Map[String, Any]) = Test(m("k1').asInstanceOf[Int], m("k2") ... )
如何呢?如果你想要一个将映射转换为任何 case class 的抽象,你可能需要使用反射。 - Rob N