Kiểu Dynamic hoạt động như thế nào và cách sử dụng nó như thế nào?


95

Tôi nghe nói rằng Dynamicbằng cách nào đó có thể thực hiện nhập động trong Scala. Nhưng tôi không thể tưởng tượng nó có thể trông như thế nào hoặc nó hoạt động như thế nào.

Tôi phát hiện ra rằng một người có thể thừa hưởng đặc điểm Dynamic

class DynImpl extends Dynamic

Các API nói rằng người ta có thể sử dụng nó như thế này:

foo.method ("blah") ~~> foo.applyDynamic ("method") ("blah")

Nhưng khi tôi dùng thử nó không hoạt động:

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")
               ^

Điều này hoàn toàn hợp lý, bởi vì sau khi tìm đến các nguồn , hóa ra đặc điểm này hoàn toàn trống rỗng. Không có phương pháp nào applyDynamicđược định nghĩa và tôi không thể tưởng tượng cách thực hiện nó một mình.

Ai đó có thể chỉ cho tôi những gì tôi cần làm để làm cho nó hoạt động?

Câu trả lời:


188

Kiểu Scalas Dynamiccho phép bạn gọi các phương thức trên các đối tượng không tồn tại hay nói cách khác nó là bản sao của "phương thức bị thiếu" trong các ngôn ngữ động.

Nó đúng, scala.Dynamickhông có bất kỳ thành viên nào, nó chỉ là một giao diện đánh dấu - việc triển khai cụ thể được trình biên dịch điền vào. Đối với tính năng Nội suy chuỗi Scalas có các quy tắc được xác định rõ ràng mô tả việc triển khai được tạo ra. Trên thực tế, người ta có thể thực hiện bốn phương pháp khác nhau:

  • selectDynamic - cho phép viết các trình truy cập trường: foo.bar
  • updateDynamic - cho phép viết cập nhật trường: foo.bar = 0
  • applyDynamic - cho phép gọi các phương thức với các đối số: foo.bar(0)
  • applyDynamicNamed - cho phép gọi các phương thức với các đối số được đặt tên: foo.bar(f = 0)

Để sử dụng một trong các phương thức này, chỉ cần viết một lớp mở rộng Dynamicvà triển khai các phương thức ở đó:

class DynImpl extends Dynamic {
  // method implementations here
}

Hơn nữa, người ta cần thêm một

import scala.language.dynamics

hoặc đặt tùy chọn trình biên dịch -language:dynamicsvì tính năng bị ẩn theo mặc định.

selectDynamic

selectDynamiclà cách dễ thực hiện nhất. Trình biên dịch dịch một lệnh gọi foo.bartới foo.selectDynamic("bar"), do đó, phương thức này yêu cầu phải có danh sách đối số mong đợi 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

Như có thể thấy, cũng có thể gọi các phương thức động một cách rõ ràng.

updateDynamic

updateDynamicđược sử dụng để cập nhật một giá trị mà phương thức này cần trả về Unit. Hơn nữa, tên của trường cần cập nhật và giá trị của nó được trình biên dịch chuyển đến các danh sách đối số khác nhau:

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

Mã hoạt động như mong đợi - có thể thêm các phương thức trong thời gian chạy vào mã. Mặt khác, mã không còn là kiểu an toàn nữa và nếu một phương thức được gọi là không tồn tại thì điều này cũng phải được xử lý trong thời gian chạy. Ngoài ra mã này không hữu ích như trong các ngôn ngữ động vì không thể tạo các phương thức nên được gọi trong thời gian chạy. Điều này có nghĩa là chúng ta không thể làm điều gì đó như

val name = "foo"
d.$name

nơi d.$namesẽ được chuyển đổi thành d.footrong thời gian chạy. Nhưng điều này cũng không tệ lắm vì ngay cả trong các ngôn ngữ động đây cũng là một tính năng nguy hiểm.

Một điều khác cần lưu ý ở đây, đó là updateDynamiccần phải được thực hiện cùng với selectDynamic. Nếu chúng ta không làm điều này, chúng ta sẽ gặp lỗi biên dịch - quy tắc này tương tự như việc triển khai Setter, chỉ hoạt động nếu có một Getter có cùng tên.

applyDynamic

Khả năng gọi các phương thức với các đối số được cung cấp bởi 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

Tên của phương thức và các đối số của nó lại được phân tách thành các danh sách tham số khác nhau. Chúng ta có thể gọi các phương thức tùy ý với một số lượng đối số tùy ý nếu chúng ta muốn nhưng nếu chúng ta muốn gọi một phương thức không có bất kỳ dấu ngoặc nào thì chúng ta cần thực hiện selectDynamic.

Gợi ý: Cũng có thể sử dụng cú pháp apply với applyDynamic:

scala> d(5)
res1: String = method 'apply' called with arguments '5'

applyDynamicNamed

Phương thức khả dụng cuối cùng cho phép chúng tôi đặt tên cho các đối số của mình nếu chúng tôi muốn:

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)'

Sự khác biệt trong phương pháp chữ ký là applyDynamicNamedhy vọng các bộ dạng (String, A)Alà một kiểu bất kỳ.


Tất cả các phương thức trên đều có điểm chung là các tham số của chúng có thể được tham số hó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

May mắn thay, cũng có thể thêm các đối số ngầm - nếu chúng ta thêm một TypeTagngữ cảnh ràng buộc, chúng ta có thể dễ dàng kiểm tra các loại đối số. Và điều tốt nhất là ngay cả kiểu trả về cũng đúng - mặc dù chúng tôi đã phải thêm một số phôi.

Nhưng Scala sẽ không phải là Scala khi không có cách nào để tìm ra cách khắc phục những sai sót đó. Trong trường hợp của chúng tôi, chúng tôi có thể sử dụng các lớp kiểu để tránh các phôi:

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: _*)
  }

}

Mặc dù việc triển khai trông không đẹp mắt nhưng không thể nghi ngờ sức mạnh của nó:

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

Trên hết, nó cũng có thể kết hợp Dynamicvới macro:

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")
                       ^

Macro cung cấp cho chúng ta tất cả các đảm bảo về thời gian biên dịch và mặc dù nó không hữu ích trong trường hợp trên, nhưng có thể nó có thể rất hữu ích đối với một số Scala DSL.

Nếu bạn muốn biết thêm thông tin về Dynamicmột số tài nguyên khác:


1
Chắc chắn là một câu trả lời tuyệt vời và một sự giới thiệu của Sức mạnh Scala
Herrington Darkholme vào

Tôi sẽ không gọi nó là sức mạnh trong trường hợp tính năng bị ẩn theo mặc định, ví dụ: có thể là thử nghiệm hoặc không chơi tốt với những người khác, hoặc có?
matanster

Có thông tin nào về hiệu suất của Scala Dynamic không? Tôi biết Scala Reflection là chậm (do đó có Scala-macro). Việc sử dụng Scala Dynamic có làm chậm hiệu suất đáng kể không?
windweller

1
@AllenNie Như bạn có thể thấy trong câu trả lời của tôi, có nhiều cách khác nhau để thực hiện nó. Nếu bạn sử dụng macro, không có chi phí nào nữa, vì lệnh gọi động được giải quyết tại thời điểm biên dịch. Nếu bạn sử dụng thực hiện kiểm tra trong thời gian chạy, bạn phải thực hiện kiểm tra tham số để gửi chính xác đến đúng đường dẫn mã. Điều đó không phải là chi phí cao hơn bất kỳ thông số nào khác mà ứng dụng của bạn có. Nếu bạn sử dụng phản xạ, bạn rõ ràng sẽ nhận được nhiều chi phí hơn nhưng bạn phải tự mình đo lường mức độ làm chậm ứng dụng của bạn.
kiritsuku

1
"Macros cho chúng ta lùi lại hoàn toàn đảm bảo thời gian biên dịch" - đây là thổi tâm trí của tôi
tksfz
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.