Scala中如何用!符号自动生成非空断言?是否有简易宏?
String! Syntax in Scala Great question—repeating assert(name != null) for every non-nullable parameter gets tedious fast, and macros are exactly the tool to automate this. Here’s how to implement your desired String! syntax to auto-inject null checks:
For Scala 2
Scala 2 uses macro annotations (requires the Macro Paradise plugin) to modify class definitions at compile time. Here’s a step-by-step implementation:
1. Set Up Dependencies
First, add the necessary dependencies to your build.sbt:
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full)
2. Define the Macro Annotation
This macro will scan your class constructor parameters, look for types ending with ! (like String!), and auto-generate null checks for them:
import scala.annotation.{StaticAnnotation, compileTimeOnly} import scala.reflect.macros.whitebox import scala.language.experimental.macros @compileTimeOnly("Enable Macro Paradise to expand this annotation") class generateNullChecks extends StaticAnnotation { def macroTransform(annottees: Any*): Any = macro GenerateNullChecksMacro.impl } object GenerateNullChecksMacro { def impl(c: whitebox.Context)(annottees: c.Tree*): c.Tree = { import c.universe._ annottees.head match { case classDef: ClassDef => // Grab all constructor parameters val constructorParams = classDef.impl.body.collect { case ctor: DefDef if ctor.name == termNames.CONSTRUCTOR => ctor.vparamss.flatten }.headOption.getOrElse(Nil) // Generate assert statements for parameters with !-suffixed types val nullChecks = constructorParams.flatMap { param => if (param.tpt.toString.endsWith("!")) { Some(q"assert(${param.name} != null, ${s"${param.name} must not be null"})") } else None } // Inject the checks into the constructor body val updatedClass = classDef.copy( impl = classDef.impl.copy( body = classDef.impl.body.map { case ctor: DefDef if ctor.name == termNames.CONSTRUCTOR => ctor.copy(body = Block(nullChecks ++ ctor.body.children, ctor.body.expr)) case other => other } ) ) c.untypecheck(updatedClass) case _ => c.abort(c.enclosingPosition, "This annotation only works on classes") } } }
3. Define the String! Type Alias
Since String! isn’t a native Scala type, define a simple alias to use as our marker:
type String! = String
4. Use It!
Annotate your class with @generateNullChecks and use String! for non-nullable parameters:
@generateNullChecks class Person(val name: String!) { // The macro auto-injects: assert(name != null, "name must not be null") }
For Scala 3
Scala 3’s metaprogramming is more streamlined (no extra plugins needed!) thanks to inline macros and the Quotes API. Here’s how to adapt the solution:
1. Implement the Inline Annotation
import scala.annotation.StaticAnnotation import scala.quoted.* class generateNullChecks extends StaticAnnotation: def transform(using Quotes)(tree: quotes.reflect.Definition): quotes.reflect.Definition = import quotes.reflect.* tree match case cls: ClassDef => // Get the class constructor val constructor = cls.constructor.getOrElse( report.errorAndAbort("Class must have a constructor", cls.pos) ) // Generate checks for !-suffixed parameters val nullChecks = constructor.paramss.flatten.flatMap { param => if (param.tpt.tpe.show.endsWith("!")) { val paramName = param.name Some('{ assert(${Expr(paramName)} != null, ${Expr(s"$paramName must not be null")}).asTerm }) } else None } // Update constructor with checks val updatedConstructor = constructor.copy( body = Some(Block(nullChecks, constructor.body.get)) ) cls.copy(constructor = Some(updatedConstructor)) case _ => report.errorAndAbort("Annotation only applies to classes", tree.pos)
2. Define the Type Alias
Same as Scala 2:
type String! = String
3. Use the Annotation
@generateNullChecks class Person(val name: String!) // Auto-generates: assert(name != null, "name must not be null") in the constructor
Notes & Alternatives
- Type Safety: The
String!alias is just a marker—if you want stricter compile-time checks, you could define a wrapper type (likecase class NotNullString(value: String)) and use macros to auto-wrap/unwrap it, but that adds more complexity. - Scala 3 Option/NonNull: For native non-null handling, Scala 3 has
NonNulltypes (via-Yexplicit-nullsflag), but that’s a different approach than your desiredString!syntax. - Runtime Overhead: Assertions are enabled by default in Scala (disable with
-Xdisable-assertions), so this adds minimal runtime cost when enabled.
内容的提问来源于stack exchange,提问作者Dmitry Nogin

