Kiểu Scalas Dynamic
cho 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.Dynamic
khô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 Dynamic
và 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:dynamics
vì tính năng bị ẩn theo mặc định.
selectDynamic
selectDynamic
là cách dễ thực hiện nhất. Trình biên dịch dịch một lệnh gọi foo.bar
tớ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
Vì 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.$name
sẽ được chuyển đổi thành d.foo
trong 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à updateDynamic
cầ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à applyDynamicNamed
hy vọng các bộ dạng (String, A)
mà A
là 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 TypeTag
ngữ 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 Dynamic
vớ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ề Dynamic
một số tài nguyên khác: