Scala 2.12.17中如何编译期自动生成Case Class的Data参数Apply方法?
可以做到,推荐用Scala Macro Paradise实现自定义注解来批量生成所需的apply方法,以下是具体步骤和代码示例:
方案一:自定义注解批量生成Apply方法
这个方案适合你有多个case class的场景,一次注解就能自动生成支持Data参数的apply方法。
1. 配置SBT依赖
Scala 2.12需要引入Macro Paradise编译器插件,在build.sbt中添加:
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full) libraryDependencies += "org.scalamacros" %% "resetallattrs" % "1.0.0"
2. 实现注解与宏逻辑
创建一个单独的文件(比如DataAwareMacro.scala),定义注解和编译期宏:
import scala.annotation.StaticAnnotation import scala.language.experimental.macros import scala.reflect.macros.blackbox // 自定义注解,标注需要生成Data参数apply方法的case class class DataAwareCaseClass extends StaticAnnotation { def macroTransform(annottees: Any*): Any = macro DataAwareCaseClassMacro.impl } // 宏实现:编译期解析case class并生成额外的apply方法 object DataAwareCaseClassMacro { def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = { import c.universe._ annottees.head match { // 匹配仅含单个String参数的case class case q"case class $className( $paramName: String )" => // 生成接收Data参数的apply方法,内部调用loadData转换 val dataApplyMethod = q""" object ${className.toTermName} { def apply(data: Data): $className = $className(loadData(data)) } """ // 合并原case class定义和新的伴生对象方法 q""" $className $dataApplyMethod """ // 不匹配时抛出编译错误 case _ => c.abort(c.enclosingPosition, "@DataAwareCaseClass仅适用于单个String参数的case class") } } }
3. 使用注解生成方法
在你的业务代码中,给目标case class加上注解即可:
// 你的原有Data定义和loadData函数 trait Data case class DoubleInt(a: Int, b: Int) extends Data case class SingleInt(n: Int) extends Data def loadData(data: Data): String = data match { case DoubleInt(a, b) => s"$a,$b" case SingleInt(n) => n.toString } // 给case class添加注解,自动生成Data参数的apply方法 @DataAwareCaseClass case class TestOne(s: String) @DataAwareCaseClass case class TestTwo(s: String) // 测试使用 val data1: Data = DoubleInt(10, 20) val test1 = TestOne(data1) // 自动执行loadData(data1),生成TestOne("10,20") println(test1) // 输出:TestOne(10,20)
方案二:手动宏扩展(单类处理)
如果只有少数几个case class,也可以手动为每个类生成apply方法,不需要注解:
import scala.language.experimental.macros import scala.reflect.macros.blackbox object DataApplyGenerator { def createApply[T]: Unit = macro createApplyImpl[T] def createApplyImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Tree = { import c.universe._ val tpe = weakTypeTag[T].tpe val className = tpe.typeSymbol.name.toTermName // 校验case class是否符合要求 val constructor = tpe.decl(termNames.CONSTRUCTOR).asMethod if (constructor.paramLists.flatten.size != 1 || constructor.paramLists.flatten.head.typeSignature != typeOf[String]) { c.abort(c.enclosingPosition, s"${tpe.typeSymbol.name}必须是仅含单个String参数的case class") } // 生成apply方法 q""" object $className { def apply(data: Data): $tpe = $className(loadData(data)) } """ } } // 使用示例:在case class的伴生对象中调用宏 case class TestThree(s: String) object TestThree extends DataApplyGenerator.createApply[TestThree] // 测试 val data2: Data = SingleInt(5) val test3 = TestThree(data2) // 生成TestThree("5")
注意事项
- 宏代码需要在编译期执行,如果是多模块项目,建议把宏代码放在单独的模块,主模块依赖该模块。
- 确保
loadData函数在宏代码的可见作用域内(比如放在公共包下),否则宏无法引用到它。 - 宏会在编译期做校验,如果case class不符合“单个String参数”的要求,会直接抛出编译错误,提前发现问题。
内容的提问来源于stack exchange,提问作者Ares
相关产品推荐
相关产品推荐

