Mit dem Skalentyp Dynamic
können Sie Methoden für Objekte aufrufen, die nicht vorhanden sind. Mit anderen Worten, es handelt sich um eine Replik von "Methode fehlt" in dynamischen Sprachen.
Es ist richtig, scala.Dynamic
hat keine Mitglieder, es ist nur eine Markierungsschnittstelle - die konkrete Implementierung wird vom Compiler ausgefüllt. Wie für Scalas String Interpolation- Funktion gibt es genau definierte Regeln, die die generierte Implementierung beschreiben. Tatsächlich kann man vier verschiedene Methoden implementieren:
selectDynamic
- ermöglicht das Schreiben von Feldzugängern: foo.bar
updateDynamic
- ermöglicht das Schreiben von Feldaktualisierungen: foo.bar = 0
applyDynamic
- Ermöglicht das Aufrufen von Methoden mit Argumenten: foo.bar(0)
applyDynamicNamed
- Ermöglicht das Aufrufen von Methoden mit benannten Argumenten: foo.bar(f = 0)
Um eine dieser Methoden zu verwenden, reicht es aus, eine erweiterte Klasse zu schreiben Dynamic
und die Methoden dort zu implementieren:
class DynImpl extends Dynamic {
// method implementations here
}
Außerdem muss man a hinzufügen
import scala.language.dynamics
oder setzen Sie die Compiler-Option -language:dynamics
da die Funktion standardmäßig ausgeblendet ist.
selectDynamic
selectDynamic
ist am einfachsten zu implementieren. Der Compiler übersetzt einen Aufruf von foo.bar
to foo.selectDynamic("bar")
, daher ist es erforderlich, dass diese Methode eine Argumentliste hat, die Folgendes erwartet 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
Wie man sieht, ist es auch möglich, die dynamischen Methoden explizit aufzurufen.
updateDynamic
Da updateDynamic
diese Methode zum Aktualisieren eines Werts verwendet wird, muss sie zurückgegeben werden Unit
. Darüber hinaus werden der Name des zu aktualisierenden Felds und sein Wert vom Compiler an verschiedene Argumentlisten übergeben:
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
Der Code funktioniert wie erwartet - es ist möglich, dem Code zur Laufzeit Methoden hinzuzufügen. Auf der anderen Seite ist der Code nicht mehr typsicher, und wenn eine Methode aufgerufen wird, die nicht existiert, muss dies auch zur Laufzeit behandelt werden. Außerdem ist dieser Code nicht so nützlich wie in dynamischen Sprachen, da es nicht möglich ist, die Methoden zu erstellen, die zur Laufzeit aufgerufen werden sollen. Das heißt, wir können so etwas nicht machen
val name = "foo"
d.$name
wo d.$name
würde zur d.foo
Laufzeit umgewandelt werden. Das ist aber nicht so schlimm, denn selbst in dynamischen Sprachen ist dies eine gefährliche Funktion.
Eine andere Sache, die hier zu beachten ist, ist, dass updateDynamic
zusammen mit implementiert werden mussselectDynamic
. Wenn wir dies nicht tun, erhalten wir einen Kompilierungsfehler - diese Regel ähnelt der Implementierung eines Setters, die nur funktioniert, wenn es einen Getter mit demselben Namen gibt.
applyDynamic
Die Möglichkeit, Methoden mit Argumenten aufzurufen, wird bereitgestellt durch 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
Der Name der Methode und ihre Argumente werden wiederum in verschiedene Parameterlisten unterteilt. Wir können beliebige Methoden mit einer beliebigen Anzahl von Argumenten aufrufen, wenn wir möchten, aber wenn wir eine Methode ohne Klammern aufrufen möchten, müssen wir sie implementierenselectDynamic
.
Hinweis: Es ist auch möglich, die Apply-Syntax zu verwenden mit applyDynamic
:
scala> d(5)
res1: String = method 'apply' called with arguments '5'
applyDynamicNamed
Mit der letzten verfügbaren Methode können wir unsere Argumente benennen, wenn wir möchten:
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)'
Der Unterschied in der Methodensignatur besteht darin, applyDynamicNamed
dass Tupel der Form erwartet werden, bei (String, A)
denen A
es sich um einen beliebigen Typ handelt.
Allen oben genannten Methoden ist gemeinsam, dass ihre Parameter parametriert werden können:
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
Glücklicherweise ist es auch möglich, implizite Argumente hinzuzufügen. Wenn wir eine TypeTag
Kontextbindung hinzufügen , können wir die Arten der Argumente leicht überprüfen. Und das Beste ist, dass sogar der Rückgabetyp korrekt ist - obwohl wir einige Casts hinzufügen mussten.
Aber Scala wäre keine Scala, wenn es keinen Weg gibt, solche Fehler zu umgehen. In unserem Fall können wir Typklassen verwenden, um die Casts zu vermeiden:
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: _*)
}
}
Obwohl die Implementierung nicht so gut aussieht, kann ihre Leistungsfähigkeit nicht in Frage gestellt werden:
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
Darüber hinaus ist es auch möglich, Dynamic
mit Makros zu kombinieren :
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")
^
Makros geben uns alle Garantien für die Kompilierungszeit zurück, und obwohl dies im obigen Fall nicht so nützlich ist, kann es für einige Scala-DSLs möglicherweise sehr nützlich sein.
Wenn Sie noch mehr Informationen darüber erhalten möchten, Dynamic
gibt es einige weitere Ressourcen: