Lớp trường hợp để lập bản đồ trong Scala


75

Có cách nào tốt để tôi có thể chuyển đổi một phiên bản Scala không case class, ví dụ:

case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")

thành một ánh xạ của một số loại, ví dụ:

getCCParams(x) returns "param1" -> "hello", "param2" -> "world"

Nó hoạt động cho bất kỳ lớp trường hợp nào, không chỉ những lớp được xác định trước. Tôi thấy rằng bạn có thể kéo tên lớp trường hợp ra bằng cách viết một phương thức thẩm vấn lớp Sản phẩm bên dưới, ví dụ:

def getCCName(caseobj: Product) = caseobj.productPrefix 
getCCName(x) returns "MyClass"

Vì vậy, tôi đang tìm kiếm một giải pháp tương tự nhưng cho các trường lớp trường hợp. Tôi đã tưởng tượng một giải pháp có thể phải sử dụng phản chiếu Java, nhưng tôi không muốn viết thứ gì đó có thể bị hỏng trong bản phát hành Scala trong tương lai nếu việc triển khai cơ bản của các lớp trường hợp thay đổi.

Hiện tại tôi đang làm việc trên máy chủ Scala và xác định giao thức cũng như tất cả các thông báo và ngoại lệ của nó bằng cách sử dụng các lớp trường hợp, vì chúng là một cấu trúc ngắn gọn, đẹp đẽ cho việc này. Nhưng sau đó tôi cần phải dịch chúng thành một bản đồ Java để gửi qua lớp nhắn tin cho bất kỳ triển khai máy khách nào sử dụng. Việc triển khai hiện tại của tôi chỉ xác định một bản dịch cho từng lớp trường hợp riêng biệt, nhưng sẽ rất tốt nếu bạn tìm thấy một giải pháp tổng quát.


Tôi đã tìm thấy bài đăng trên blog này hướng dẫn cách sử dụng macro để thực hiện việc này.
Giovanni Botta

Câu trả lời:


93

Điều này sẽ hoạt động:

def getCCParams(cc: AnyRef) =
  cc.getClass.getDeclaredFields.foldLeft(Map.empty[String, Any]) { (a, f) =>
    f.setAccessible(true)
    a + (f.getName -> f.get(cc))
  }

15
Nếu điều này không khó, bạn có thể giải thích những gì bạn đã viết?
den bardadym

Bây giờ có phản xạ vảy! Tôi không chắc liệu nó vẫn đang thử nghiệm hay nó đã ổn định hay chưa. Dù sao thì có thể API phản xạ theo tỉ lệ cung cấp một giải pháp riêng hoặc ít nhất là một cách khác theo tỉ lệ để triển khai một giải pháp như ở trên. Và nhân tiện: khi bạn sử dụng setAccessible thành true, bạn cũng có thể truy cập vào các trường riêng tư. Đây thực sự là những gì bạn muốn? Và nó có thể không hoạt động khi SecurityManager đang hoạt động.
dùng573215

2
@RobinGreen lớp trường hợp không thể kế thừa từ nhau
Giovanni Botta

1
@GiovanniBotta Có vẻ như vấn đề là phương pháp trình truy cập không được đánh dấu là có thể truy cập được. Kỳ lạ, vì nó là một phương pháp công khai. Trong mọi trường hợp, bạn nên lấy ra isAccessible, vì getMethodchỉ trả về các phương thức công khai. Ngoài ra, accessor != nulllà một thử nghiệm sai, như getMethodném một NoSuchMethodExceptionnếu phương pháp không được tìm thấy.
James_pic

2
lẽ theo cách này là dễ hiểu: case class Person(name: String, surname: String) val person = new Person("Daniele", "DalleMule") val personAsMap = person.getClass.getDeclaredFields.foldLeft(Map[String, Any]())((map, field) => { field.setAccessible(true) map + (field.getName -> field.get(person)) } )
DanieleDM

42

Bởi vì các lớp trường hợp mở rộng Sản phẩm, người ta có thể đơn giản sử dụng .productIteratorđể nhận các giá trị trường:

def getCCParams(cc: Product) = cc.getClass.getDeclaredFields.map( _.getName ) // all field names
                .zip( cc.productIterator.to ).toMap // zipped with all values

Hay cách khác:

def getCCParams(cc: Product) = {          
      val values = cc.productIterator
      cc.getClass.getDeclaredFields.map( _.getName -> values.next ).toMap
}

Một ưu điểm của Sản phẩm là bạn không cần gọi setAccessible trên trường để đọc giá trị của nó. Một điều khác là productIterator không sử dụng phản chiếu.

Lưu ý rằng ví dụ này hoạt động với các lớp trường hợp đơn giản không mở rộng các lớp khác và không khai báo các trường bên ngoài phương thức khởi tạo.


7
Đặc getDeclaredFieldstả cho biết: "Các phần tử trong mảng được trả về không được sắp xếp và không theo bất kỳ thứ tự cụ thể nào." Làm thế nào để các trường trả về theo đúng thứ tự?
Giovanni Botta

Đúng, tốt nhất bạn nên kiểm tra jvm / os của mình nhưng trên thực tế stackoverflow.com/a/5004929/1180621
Andrejs

2
Vâng, tôi sẽ không coi đó là điều hiển nhiên. Tôi không muốn bắt đầu viết mã không di động.
Giovanni Botta

Điều này sẽ ném ra một ngoại lệ nếu lớp trường hợp được lồng trong một đối tượng khác vì productIterator sẽ không chứa trường được khai báo "$ external".
ssice

20

Bắt đầu Scala 2.13, các case classes (như các triển khai của Product) được cung cấp với một phương thức productElementNames trả về một trình lặp trên tên trường của chúng.

Bằng cách nén các tên trường với các giá trị trường thu được bằng productIterator, về cơ bản, chúng ta có thể nhận được Map:

// case class MyClass(param1: String, param2: String)
// val x = MyClass("hello", "world")
(x.productElementNames zip x.productIterator).toMap
// Map[String,Any] = Map("param1" -> "hello", "param2" -> "world")

12

Nếu ai đó đang tìm kiếm một phiên bản đệ quy, đây là phần sửa đổi giải pháp của @ Andrejs:

def getCCParams(cc: Product): Map[String, Any] = {
  val values = cc.productIterator
  cc.getClass.getDeclaredFields.map {
    _.getName -> (values.next() match {
      case p: Product if p.productArity > 0 => getCCParams(p)
      case x => x
    })
  }.toMap
}

Nó cũng mở rộng các lớp trường hợp lồng nhau thành các bản đồ ở bất kỳ mức độ lồng nào.


6

Đây là một biến thể đơn giản nếu bạn không quan tâm đến việc biến nó thành một hàm chung:

case class Person(name:String, age:Int)

def personToMap(person: Person): Map[String, Any] = {
  val fieldNames = person.getClass.getDeclaredFields.map(_.getName)
  val vals = Person.unapply(person).get.productIterator.toSeq
  fieldNames.zip(vals).toMap
}

scala> println(personToMap(Person("Tom", 50)))
res02: scala.collection.immutable.Map[String,Any] = Map(name -> Tom, age -> 50)

4

Giải pháp với ProductCompletiontừ gói thông dịch viên:

import tools.nsc.interpreter.ProductCompletion

def getCCParams(cc: Product) = {
  val pc = new ProductCompletion(cc)
  pc.caseNames.zip(pc.caseFields).toMap
}

5
Tools.nsc.interpreter.ProductCompletion có bị chuyển đi nơi khác trong Scala 2.10 không?
pdxleif

4

Bạn có thể sử dụng shapeless.

Để cho

case class X(a: Boolean, b: String,c:Int)
case class Y(a: String, b: String)

Xác định một biểu diễn LabellingGeneric

import shapeless._
import shapeless.ops.product._
import shapeless.syntax.std.product._
object X {
  implicit val lgenX = LabelledGeneric[X]
}
object Y {
  implicit val lgenY = LabelledGeneric[Y]
}

Xác định hai kiểu chữ để cung cấp các phương thức toMap

object ToMapImplicits {

  implicit class ToMapOps[A <: Product](val a: A)
    extends AnyVal {
    def mkMapAny(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, Any] =
      a.toMap[Symbol, Any]
        .map { case (k: Symbol, v) => k.name -> v }
  }

  implicit class ToMapOps2[A <: Product](val a: A)
    extends AnyVal {
    def mkMapString(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, String] =
      a.toMap[Symbol, Any]
        .map { case (k: Symbol, v) => k.name -> v.toString }
  }
}

Sau đó, bạn có thể sử dụng nó như thế này.

object Run  extends App {
  import ToMapImplicits._
  val x: X = X(true, "bike",26)
  val y: Y = Y("first", "second")
  val anyMapX: Map[String, Any] = x.mkMapAny
  val anyMapY: Map[String, Any] = y.mkMapAny
  println("anyMapX = " + anyMapX)
  println("anyMapY = " + anyMapY)

  val stringMapX: Map[String, String] = x.mkMapString
  val stringMapY: Map[String, String] = y.mkMapString
  println("anyMapX = " + anyMapX)
  println("anyMapY = " + anyMapY)
}

cái nào in

anyMapX = Bản đồ (c -> 26, b -> xe đạp, a -> true)

anyMapY = Bản đồ (b -> thứ hai, a -> thứ nhất)

stringMapX = Bản đồ (c -> 26, b -> xe đạp, a -> true)

stringMapY = Bản đồ (b -> thứ hai, a -> thứ nhất)

Đối với các lớp trường hợp lồng nhau, (do đó, các bản đồ lồng nhau) hãy kiểm tra một câu trả lời khác


4

Nếu bạn đang sử dụng Json4s, bạn có thể làm như sau:

import org.json4s.{Extraction, _}

case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")

Extraction.decompose(x)(DefaultFormats).values.asInstanceOf[Map[String,String]]

2

Tôi không biết về đẹp ... nhưng điều này có vẻ hiệu quả, ít nhất là đối với ví dụ rất cơ bản này. Nó có thể cần một số công việc nhưng có thể đủ để bạn bắt đầu? Về cơ bản, nó lọc ra tất cả các phương thức "đã biết" từ một lớp trường hợp (hoặc bất kỳ lớp nào khác: /)

object CaseMappingTest {
  case class MyCase(a: String, b: Int)

  def caseClassToMap(obj: AnyRef) = {
    val c = obj.getClass
    val predefined = List("$tag", "productArity", "productPrefix", "hashCode",
                          "toString")
    val casemethods = c.getMethods.toList.filter{
      n =>
        (n.getParameterTypes.size == 0) &&
        (n.getDeclaringClass == c) &&
        (! predefined.exists(_ == n.getName))

    }
    val values = casemethods.map(_.invoke(obj, null))
    casemethods.map(_.getName).zip(values).foldLeft(Map[String, Any]())(_+_)
  }

  def main(args: Array[String]) {
    println(caseClassToMap(MyCase("foo", 1)))
    // prints: Map(a -> foo, b -> 1)
  }
}

2
Giáo sư. Tôi đã bỏ lỡ Class.getDeclaredFields.
André Laszlo


0

Với việc sử dụng phản chiếu Java, nhưng không thay đổi cấp độ truy cập. Chuyển đổi Product và lớp trường hợp thành Map[String, String]:

def productToMap[T <: Product](obj: T, prefix: String): Map[String, String] = {
  val clazz = obj.getClass
  val fields = clazz.getDeclaredFields.map(_.getName).toSet
  val methods = clazz.getDeclaredMethods.filter(method => fields.contains(method.getName))
  methods.foldLeft(Map[String, String]()) { case (acc, method) =>
    val value = method.invoke(obj).toString
    val key = if (prefix.isEmpty) method.getName else s"${prefix}_${method.getName}"
    acc + (key -> value)
  }
}
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.