斯卡拉类型Dynamic
允许您调用不存在的对象上的方法,或者换句话说,它是动态语言中“方法缺失”的复制品。
它是正确的,scala.Dynamic没有任何成员,它只是一个标记接口 - 具体实现由编译器填充。至于斯卡拉字符串插值具有描述生成的实现的明确定义的规则。事实上,可以实现四种不同的方法:
-
selectDynamic
- 允许编写字段访问器:foo.bar
-
updateDynamic
- 允许写入字段更新:foo.bar = 0
-
applyDynamic
- 允许使用参数调用方法:foo.bar(0)
-
applyDynamicNamed
- 允许使用命名参数调用方法:foo.bar(f = 0)
要使用这些方法之一,编写一个扩展的类就足够了Dynamic
并在那里实现方法:
class DynImpl extends Dynamic {
// method implementations here
}
此外还需要添加一个
import scala.language.dynamics
或者设置编译器选项-language:dynamics
因为该功能默认是隐藏的。
选择动态
selectDynamic
是最容易实施的一种。编译器将调用翻译为foo.bar
to foo.selectDynamic("bar")
,因此要求该方法有一个参数列表,期望String
:
class DynImpl extends Dynamic {
def selectDynamic(name: String) = name
}
scala> val d = new DynImpl
d: DynImpl = DynImpl@6040af64
scala> d.foo
res37: String = foo
scala> d.bar
res38: String = bar
scala> d.selectDynamic("foo")
res54: String = foo
正如我们所看到的,也可以显式调用动态方法。
更新动态
Because updateDynamic
用于更新该方法需要返回的值Unit
。此外,要更新的字段的名称及其值由编译器传递到不同的参数列表:
class DynImpl extends Dynamic {
var map = Map.empty[String, Any]
def selectDynamic(name: String) =
map get name getOrElse sys.error("method not found")
def updateDynamic(name: String)(value: Any) {
map += name -> value
}
}
scala> val d = new DynImpl
d: DynImpl = DynImpl@7711a38f
scala> d.foo
java.lang.RuntimeException: method not found
scala> d.foo = 10
d.foo: Any = 10
scala> d.foo
res56: Any = 10
代码按预期工作 - 可以在运行时向代码添加方法。另一方面,代码不再是类型安全的,如果调用的方法不存在,则也必须在运行时处理。此外,此代码不像动态语言中那样有用,因为不可能创建应在运行时调用的方法。这意味着我们不能做类似的事情
val name = "foo"
d.$name
where d.$name
将转变为d.foo
在运行时。但这并没有那么糟糕,因为即使在动态语言中,这也是一个危险的功能。
这里要注意的另一件事是updateDynamic
需要与selectDynamic
。如果我们不这样做,我们会得到一个编译错误 - 这个规则类似于 Setter 的实现,只有在存在同名的 Getter 时才有效。
应用动态
使用参数调用方法的能力由applyDynamic
:
class DynImpl extends Dynamic {
def applyDynamic(name: String)(args: Any*) =
s"method '$name' called with arguments ${args.mkString("'", "', '", "'")}"
}
scala> val d = new DynImpl
d: DynImpl = DynImpl@766bd19d
scala> d.ints(1, 2, 3)
res68: String = method 'ints' called with arguments '1', '2', '3'
scala> d.foo()
res69: String = method 'foo' called with arguments ''
scala> d.foo
<console>:19: error: value selectDynamic is not a member of DynImpl
方法的名称及其参数再次分为不同的参数列表。如果我们愿意的话,我们可以使用任意数量的参数调用任意方法,但是如果我们想调用一个没有任何括号的方法,我们需要实现selectDynamic
.
提示:也可以将 apply-syntax 与applyDynamic
:
scala> d(5)
res1: String = method 'apply' called with arguments '5'
应用动态命名
最后一个可用的方法允许我们根据需要命名我们的参数:
class DynImpl extends Dynamic {
def applyDynamicNamed(name: String)(args: (String, Any)*) =
s"method '$name' called with arguments ${args.mkString("'", "', '", "'")}"
}
scala> val d = new DynImpl
d: DynImpl = DynImpl@123810d1
scala> d.ints(i1 = 1, i2 = 2, 3)
res73: String = method 'ints' called with arguments '(i1,1)', '(i2,2)', '(,3)'
方法签名的区别在于applyDynamicNamed
需要以下形式的元组(String, A)
where A
是任意类型。
上述所有方法的共同点是它们的参数都可以参数化:
class DynImpl extends Dynamic {
import reflect.runtime.universe._
def applyDynamic[A : TypeTag](name: String)(args: A*): A = name match {
case "sum" if typeOf[A] =:= typeOf[Int] =>
args.asInstanceOf[Seq[Int]].sum.asInstanceOf[A]
case "concat" if typeOf[A] =:= typeOf[String] =>
args.mkString.asInstanceOf[A]
}
}
scala> val d = new DynImpl
d: DynImpl = DynImpl@5d98e533
scala> d.sum(1, 2, 3)
res0: Int = 6
scala> d.concat("a", "b", "c")
res1: String = abc
幸运的是,还可以添加隐式参数 - 如果我们添加一个TypeTag上下文绑定我们可以轻松检查参数的类型。最好的事情是,即使返回类型也是正确的 - 尽管我们必须添加一些强制转换。
但如果无法找到解决此类缺陷的方法,那么 Scala 就不再是 Scala。在我们的例子中,我们可以使用类型类来避免强制转换:
object DynTypes {
sealed abstract class DynType[A] {
def exec(as: A*): A
}
implicit object SumType extends DynType[Int] {
def exec(as: Int*): Int = as.sum
}
implicit object ConcatType extends DynType[String] {
def exec(as: String*): String = as.mkString
}
}
class DynImpl extends Dynamic {
import reflect.runtime.universe._
import DynTypes._
def applyDynamic[A : TypeTag : DynType](name: String)(args: A*): A = name match {
case "sum" if typeOf[A] =:= typeOf[Int] =>
implicitly[DynType[A]].exec(args: _*)
case "concat" if typeOf[A] =:= typeOf[String] =>
implicitly[DynType[A]].exec(args: _*)
}
}
虽然实现看起来不太好,但它的功能是不容质疑的:
scala> val d = new DynImpl
d: DynImpl = DynImpl@24a519a2
scala> d.sum(1, 2, 3)
res89: Int = 6
scala> d.concat("a", "b", "c")
res90: String = abc
最重要的是,还可以组合Dynamic
使用宏:
class DynImpl extends Dynamic {
import language.experimental.macros
def applyDynamic[A](name: String)(args: A*): A = macro DynImpl.applyDynamic[A]
}
object DynImpl {
import reflect.macros.Context
import DynTypes._
def applyDynamic[A : c.WeakTypeTag](c: Context)(name: c.Expr[String])(args: c.Expr[A]*) = {
import c.universe._
val Literal(Constant(defName: String)) = name.tree
val res = defName match {
case "sum" if weakTypeOf[A] =:= weakTypeOf[Int] =>
val seq = args map(_.tree) map { case Literal(Constant(c: Int)) => c }
implicitly[DynType[Int]].exec(seq: _*)
case "concat" if weakTypeOf[A] =:= weakTypeOf[String] =>
val seq = args map(_.tree) map { case Literal(Constant(c: String)) => c }
implicitly[DynType[String]].exec(seq: _*)
case _ =>
val seq = args map(_.tree) map { case Literal(Constant(c)) => c }
c.abort(c.enclosingPosition, s"method '$defName' with args ${seq.mkString("'", "', '", "'")} doesn't exist")
}
c.Expr(Literal(Constant(res)))
}
}
scala> val d = new DynImpl
d: DynImpl = DynImpl@c487600
scala> d.sum(1, 2, 3)
res0: Int = 6
scala> d.concat("a", "b", "c")
res1: String = abc
scala> d.noexist("a", "b", "c")
<console>:11: error: method 'noexist' with args 'a', 'b', 'c' doesn't exist
d.noexist("a", "b", "c")
^
宏为我们提供了所有编译时间保证,虽然它在上述情况下没有那么有用,但也许它对于某些 Scala DSL 非常有用。
如果您想获得更多信息Dynamic
还有更多资源:
- The 官方SIP提案那介绍了
Dynamic
进入斯卡拉
-
Scala 中动态类型的实际使用- 关于SO的另一个问题(但非常过时)