在运行时从scala中的case类对象中检索fieldname和value

时间:2015-11-24 09:19:24

标签: scala scala-reflect

示例案例类:尝试创建通用查询构建器

case class BaseQuery(operand: String, value: String)
case class ContactQuery(phone: BaseQuery, address: BaseQuery)
case class UserQuery(id: BaseQuery, name: BaseQuery, contact: ContactQuery)

val user = UserQuery(BaseQuery("equal","1"), BaseQuery("like","Foo"), ContactQuery(BaseQuery("eq","007-0000"),BaseQuery("like", "Foo City")))


//case class Contact(phone: String, address: String)

//case class User(id: Long, name: String, contact: Contact)

//val user = User(1, "Foo Dev", Contact("007-0000","Foo City"))

我们如何在scala中检索字段名称和相应的值,

进一步研究,

使用Scala反射的解决方案:

def classAccessors[T: TypeTag]: List[MethodSymbol] = typeOf[T].members.collect {
    case m: MethodSymbol if m.isCaseAccessor => m
}.toList

// The above snippet returns the field names, and as input we have to 
//feed the 'TypeTag' of the case class. And since we are only feeding a   
//TypeTag we will not have access to any object values. Is there any Scala
// Reflection variant of the solution.

另一种解决方案,

def getMapFromCaseClass(cc: AnyRef) =
(scala.collection.mutable.Map[String, Any]() /: cc.getClass.getDeclaredFields)
{
    (a, f) =>
        f.setAccessible(true)
        a + (f.getName -> f.get(cc))
}

// The above snippet returns a Map[String, Any] if we feed the case class 
//object. And we will need to match the map value to any of our other case 
//classes. If the class structure were simple, the above solution would be 
//helpful, but in case of complex case class this would not be the most efficient solution.

尝试:

I am actually trying to retrieve the list of field and their values during runtime.

One of the use cases,

val list: Seq[Somehow(Field+Value)] = listingFieldWithValue(obj: UserQuery)
for(o <- list){
o.field.type match{
case typeOne: FooType =>{
//Do something
}
case typeTwo: FooBarType =>{
//Do something else
}
}

}

1 个答案:

答案 0 :(得分:5)

不是scala-reflect解决方案,而是shapeless解决方案。 Shapeless建立在隐式宏之上,因此如果在编译时有类型信息,它比任何反射都要快。

为了实现更新的要求,您需要一些时间来携带所需的类型信息

sealed trait TypeInfo[T]{
  //include here thing you like to handle at runtime
}

接下来,您可以为不同类型定义此类信息并提供隐式解决方案。在我的例子中,这种实现也适用于以后的匹配

implicit case object StringField extends TypeInfo[String]

case class NumericField[N](implicit val num: Numeric[N]) extends TypeInfo[N]
implicit def numericField[N](implicit num: Numeric[N]): TypeInfo[N] = new NumericField[N]

case class ListField[E]() extends TypeInfo[List[E]]
implicit def listField[E] = new ListField[E]

然后我们定义更适合模式匹配结果结构

sealed trait FieldDef
case class PlainField[T](value: Any, info: TypeInfo[T]) extends FieldDef
case class EmbeddedType(values: Map[Symbol, FieldDef]) extends FieldDef

因此,任何结果都将是具有所需类型信息的值容器或用于深入查看的容器。

最后我们可以定义概念实现

import shapeless._
import shapeless.labelled.FieldType
import shapeless.ops.hlist.{ToTraversable, Mapper}

sealed abstract class ClassAccessors[C] {
  def apply(c: C): Map[Symbol, FieldDef]
}

trait LowPriorClassInfo extends Poly1 {
  implicit def fieldInfo[K <: Symbol, V](implicit witness: Witness.Aux[K], info: TypeInfo[V]) =
    at[FieldType[K, V]](f => (witness.value: Symbol, PlainField(f, info)))
}

object classInfo extends LowPriorClassInfo {
  implicit def recurseInfo[K <: Symbol, V](implicit witness: Witness.Aux[K], acc: ClassAccessors[V]) =
    at[FieldType[K, V]](f => (witness.value: Symbol, EmbeddedType(acc(f))))
}

implicit def provideClassAccessors[C, L <: HList, ML <: HList]
(implicit lgen: LabelledGeneric.Aux[C, L],
 map: Mapper.Aux[classInfo.type, L, ML],
 toList: ToTraversable.Aux[ML, List, (Symbol, FieldDef)]) =
  new ClassAccessors[C] {
    def apply(c: C) = toList(map(lgen.to(c))).toMap
  }

def classAccessors[C](c: C)(implicit acc: ClassAccessors[C]) = acc(c)

现在

classAccessors(User(100, "Miles Sabin", Contact("+1 234 567 890", "Earth, TypeLevel Inc., 10")))

将导致

 Map(
 'id -> PlainField(100, NumericField( scala.math.Numeric$LongIsIntegral$@...)),
 'name -> PlainField("Miles Sabin", StringField), 
 'contact -> EmbeddedType( Map(
      'phone -> PlainField( "+1 234 567 890", StringField), 
      'address -> PlainField("Earth, TypeLevel Inc., 10", StringField))))