type Dynamic ทำงานอย่างไรและจะใช้งานอย่างไร?


98

ฉันได้ยินมาว่าDynamicมันเป็นไปได้ที่จะพิมพ์แบบไดนามิกใน Scala แต่ฉันนึกไม่ออกว่ามันจะเป็นยังไงหรือมันทำงานอย่างไร

ฉันพบว่าใคร ๆ ก็สามารถสืบทอดจากลักษณะ Dynamic

class DynImpl extends Dynamic

APIบอกว่าหนึ่งสามารถใช้มันเช่นนี้

foo.method ("blah") ~~> foo.applyDynamic ("วิธีการ") ("blah")

แต่เมื่อลองใช้แล้วไม่ได้ผล:

scala> (new DynImpl).method("blah")
<console>:17: error: value applyDynamic is not a member of DynImpl
error after rewriting to new DynImpl().<applyDynamic: error>("method")
possible cause: maybe a wrong Dynamic method signature?
              (new DynImpl).method("blah")
               ^

นี่เป็นเหตุผลอย่างสมบูรณ์เพราะหลังจากค้นหาแหล่งที่มาแล้วปรากฎว่าลักษณะนี้ว่างเปล่าโดยสิ้นเชิง ไม่มีวิธีการที่applyDynamicกำหนดไว้และฉันนึกไม่ออกว่าจะนำไปปฏิบัติด้วยตัวเองอย่างไร

ใครช่วยแสดงสิ่งที่ต้องทำเพื่อให้ใช้งานได้

คำตอบ:


192

ประเภท Scalas Dynamicช่วยให้คุณสามารถเรียกใช้เมธอดบนวัตถุที่ไม่มีอยู่หรืออีกนัยหนึ่งก็คือแบบจำลองของ "method missing" ในภาษาไดนามิก

ถูกต้องscala.Dynamicไม่มีสมาชิกเป็นเพียงอินเทอร์เฟซเครื่องหมาย - การใช้งานที่เป็นรูปธรรมถูกเติมเต็มโดยคอมไพเลอร์ สำหรับคุณลักษณะการแก้ไขสตริง Scalas มีกฎที่กำหนดไว้อย่างดีซึ่งอธิบายถึงการนำไปใช้งานที่สร้างขึ้น ในความเป็นจริงหนึ่งสามารถใช้สี่วิธีที่แตกต่างกัน:

  • 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

selectDynamicเป็นวิธีที่ง่ายที่สุดในการนำไปใช้ คอมไพเลอร์แปล call of foo.barto foo.selectDynamic("bar")ดังนั้นจึงจำเป็นต้องใช้เมธอดนี้มีรายการอาร์กิวเมนต์ที่คาดหวัง a 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

อย่างที่เราเห็นมันเป็นไปได้ที่จะเรียกวิธีการแบบไดนามิกอย่างชัดเจน

updateDynamic

เพราะจะใช้ในการปรับปรุงค่าวิธีการนี้จะต้องกลับมา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

ที่d.$nameจะเปลี่ยนเป็นd.fooรันไทม์ แต่นี่ก็ไม่ได้แย่ขนาดนั้นเพราะแม้แต่ในภาษาไดนามิกนี่ก็เป็นคุณสมบัติที่อันตราย

สิ่งที่จะต้องทราบที่นี่อีกนั่นคือความต้องการที่จะดำเนินการร่วมกับupdateDynamic selectDynamicหากเราไม่ทำเช่นนี้เราจะได้รับข้อผิดพลาดในการคอมไพล์ - กฎนี้คล้ายกับการใช้งาน Setter ซึ่งใช้ได้เฉพาะเมื่อมี Getter ที่มีชื่อเดียวกัน

ApplyDynamic

ความสามารถในการเรียกเมธอดพร้อมอาร์กิวเมนต์มีให้โดย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'

applyDynamicNamed

วิธีสุดท้ายที่ใช้ได้ช่วยให้เราตั้งชื่ออาร์กิวเมนต์ได้หากต้องการ:

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)ที่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แหล่งข้อมูลเพิ่มเติม:


1
คำตอบที่ยอดเยี่ยมแน่นอนและการแสดงของ Scala Power
Herrington Darkholme

ฉันจะไม่เรียกมันว่าพลังในกรณีที่คุณลักษณะนี้ถูกซ่อนไว้โดยค่าเริ่มต้นเช่นอาจเป็นการทดลองหรือเล่นกับผู้อื่นได้ไม่ดีหรือไม่
matanster

มีข้อมูลเกี่ยวกับประสิทธิภาพของ Scala Dynamic หรือไม่? ฉันรู้ว่า Scala Reflection นั้นช้า (Scala-macro มา) การใช้ Scala Dynamic จะทำให้ประสิทธิภาพช้าลงอย่างมากหรือไม่?
windweller

1
@AllenNie ดังที่คุณเห็นในคำตอบของฉันมีหลายวิธีในการนำไปใช้ หากคุณใช้มาโครจะไม่มีค่าใช้จ่ายอีกต่อไปเนื่องจากการเรียกแบบไดนามิกได้รับการแก้ไขในเวลาคอมไพล์ หากคุณใช้ do check ที่รันไทม์คุณต้องทำการตรวจสอบพารามิเตอร์เพื่อส่งไปยังเส้นทางรหัสที่ถูกต้อง ไม่ควรมีค่าใช้จ่ายเกินกว่าพารามิเตอร์อื่น ๆ ที่ตรวจสอบในแอปพลิเคชันของคุณ หากคุณใช้ประโยชน์จากการสะท้อนคุณจะเห็นได้ชัดว่ามีค่าใช้จ่ายเพิ่มขึ้น แต่คุณต้องวัดด้วยตัวเองว่าแอปพลิเคชันของคุณทำงานช้าลงมากเพียงใด
kiritsuku

1
"มาโครให้การรับประกันเวลาคอมไพล์คืนแก่เรา" - นี่เป็นเรื่องที่น่าสนใจ
tksfz
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.