How can I create an instance of a Case Class with

2019-01-26 17:31发布

I'm making a Scala app that sets by reflection field values. This works OK.

However, in order to set field values I need a created instance. If I have a class with an empty constructor, I can do this easily with classOf[Person].getConstructors....

However, when I try doing this with a Case class with a non empty constructor It doesn't work. I have all of the field names and its values, as well as the Object type I need to create. Can I instance the Case Class somehow with what I've got?

The only thing I don't have is the parameter names from the Case Class constructor or a way to create this without parameter and then setting the values via reflection.

Let's go to the example.

I have the following

case class Person(name : String, age : Int)
class Dog(name : String) {
    def this() = {
        name = "Tony"
    }
}

class Reflector[O](obj : O) {

    def setValue[F](propName : String, value : F) = ...

    def getValue(propName : String) = ...
}

//This works
val dog = classOf[Dog].newInstance()
new Reflector(dog).setValue("name", "Doggy")

//This doesn't
val person = classOf[Person].newInstance //Doesn't work

val ctor = classOf[Person].getConstructors()(0)
val ctor.newInstance(parameters) //I have the property names and values, but I don't know 
// which of them is for each parameter, nor I name the name of the constructor parameters

4条回答
欢心
2楼-- · 2019-01-26 17:39

The approach below works for any Scala class that has either a no-arg ctor or has an all defaulted primary ctor.

It makes fewer assumptions than some others about how much information is available at the point of call as all it needs is a Class[_] instance and not implicits etc. Also the approach does not rely on the class having to be a case class or having a companion at all.

FYI During construction precedence is given to the no-arg ctor if present.

object ClassUtil {

def newInstance(cz: Class[_ <: AnyRef]): AnyRef = {

    val bestCtor = findNoArgOrPrimaryCtor(cz)
    val defaultValues = getCtorDefaultArgs(cz, bestCtor)

    bestCtor.newInstance(defaultValues: _*).asInstanceOf[A]
  }

  private def defaultValueInitFieldName(i: Int): String = s"$$lessinit$$greater$$default$$${i + 1}"

  private def findNoArgOrPrimaryCtor(cz: Class[_]): Constructor[_] = {
    val ctors = cz.getConstructors.sortBy(_.getParameterTypes.size)

    if (ctors.head.getParameterTypes.size == 0) {
      // use no arg ctor
      ctors.head
    } else {
      // use primary ctor
      ctors.reverse.head
    }
  }

  private def getCtorDefaultArgs(cz: Class[_], ctor: Constructor[_]): Array[AnyRef] = {

    val defaultValueMethodNames = ctor.getParameterTypes.zipWithIndex.map {
      valIndex => defaultValueInitFieldName(valIndex._2)
    }

    try {
      defaultValueMethodNames.map(cz.getMethod(_).invoke(null))
    } catch {
      case ex: NoSuchMethodException =>
        throw new InstantiationException(s"$cz must have a no arg constructor or all args must be defaulted")
    }
  }
}
查看更多
我想做一个坏孩纸
3楼-- · 2019-01-26 17:41

I ran into a similar problem. Given the ease of using Macro Paradise, Macro Annotations are a solution (for scala 2.10.X and 2.11 so far).

Check out this question and the example project linked in the comments below.

查看更多
爷、活的狠高调
4楼-- · 2019-01-26 17:46

If you are looking for a way to instantiate the object with no arguments, you could do the same as you did in your example, just so long as your reflection setter can handle setting the immutable vals.

You would provide an alternate constructor, as below:

case class Person(name : String, age : Int) {
    def this() = this("", 0)
}

Note that the case class will not generate a zero-arg companion object, so you will need to instantiate it as: new Person() or classOf[Person].newInstance(). However, that should be what you are looking to do.

Should give you output like:

scala> case class Person(name : String, age : Int) {
     |         def this() = this("", 0)
     |     }
defined class Person

scala> classOf[Person].newInstance()
res3: Person = Person(,0)
查看更多
Anthone
5楼-- · 2019-01-26 18:04

The case class should have default args, so that you can just Person(); in the absence of a default arg, supplying a null for name might (or ought to) hit a require(name != null).

Alternatively, use reflection to figure out which params have defaults and then supply nulls or zeros for the rest.

import reflect._
import scala.reflect.runtime.{ currentMirror => cm }
import scala.reflect.runtime.universe._

// case class instance with default args

// Persons entering this site must be 18 or older, so assume that
case class Person(name: String, age: Int = 18) {
  require(age >= 18)
}

object Test extends App {

  // Person may have some default args, or not.
  // normally, must Person(name = "Guy")
  // we will Person(null, 18)
  def newCase[A]()(implicit t: ClassTag[A]): A = {
    val claas = cm classSymbol t.runtimeClass
    val modul = claas.companionSymbol.asModule
    val im = cm reflect (cm reflectModule modul).instance
    defaut[A](im, "apply")
  }

  def defaut[A](im: InstanceMirror, name: String): A = {
    val at = newTermName(name)
    val ts = im.symbol.typeSignature
    val method = (ts member at).asMethod

    // either defarg or default val for type of p
    def valueFor(p: Symbol, i: Int): Any = {
      val defarg = ts member newTermName(s"$name$$default$$${i+1}")
      if (defarg != NoSymbol) {
        println(s"default $defarg")
        (im reflectMethod defarg.asMethod)()
      } else {
        println(s"def val for $p")
        p.typeSignature match {
          case t if t =:= typeOf[String] => null
          case t if t =:= typeOf[Int]    => 0
          case x                        => throw new IllegalArgumentException(x.toString)
        }
      }
    }
    val args = (for (ps <- method.paramss; p <- ps) yield p).zipWithIndex map (p => valueFor(p._1,p._2))
    (im reflectMethod method)(args: _*).asInstanceOf[A]
  }

  assert(Person(name = null) == newCase[Person]())
}
查看更多
登录 后发表回答