将通用伴随对象传递给超级构造函数

Passing generic companion object to super constructor

我正在尝试构造一个 trait 和一个 abstract class 以按消息进行子类型化(在 Akka 游戏环境中),以便我可以轻松地将它们转换为 Json

到目前为止做了什么:

    abstract class OutputMessage(val companion: OutputMessageCompanion[OutputMessage]) {
        def toJson: JsValue =  Json.toJson(this)(companion.fmt)
    }


    trait OutputMessageCompanion[OT] {
        implicit val fmt: OFormat[OT]
    }

问题是,当我尝试按如下方式实现上述接口时:

    case class NotifyTableChange(tableStatus: BizTable) extends OutputMessage(NotifyTableChange)

    object NotifyTableChange extends OutputMessageCompanion[NotifyTableChange] {
        override implicit val fmt: OFormat[NotifyTableChange] = Json.format[NotifyTableChange]
    }

我从 Intellij 得到这个错误: Type mismatch, expected: OutputMessageCompanion[OutputMessage], actual: NotifyTableChange.type

我对 Scala 泛型有点陌生 - 非常感谢您提供一些解释。

P.S 我愿意接受比上述解决方案更通用的解决方案。 目标是,在获取 OutputMessage 的任何子类型时 - 轻松将其转换为 Json.

编译器说您的 companion 是在 OutputMessage 上定义为通用参数而不是某些特定的子类型。要解决这个问题,您需要使用一个称为 F-bound generic 的技巧。此外,我不喜欢在每条消息中将该伴随对象存储为 val 的想法(毕竟您不希望它序列化,是吗?)。将其定义为 def 恕我直言,这是更好的权衡。代码会像这样(同伴保持不变):

abstract class OutputMessage[M <: OutputMessage[M]]() {
    self: M => // required to match Json.toJson signature

    protected def companion: OutputMessageCompanion[M]

    def toJson: JsValue =  Json.toJson(this)(companion.fmt)
}

case class NotifyTableChange(tableStatus: BizTable) extends OutputMessage[NotifyTableChange] {

    override protected def companion: OutputMessageCompanion[NotifyTableChange] = NotifyTableChange
}

您可能还会看到用于实现相同方法的标准 Scala 集合。

但是如果你需要 companion 只是用 JSON 格式编码,你可以像这样去掉它:

  abstract class OutputMessage[M <: OutputMessage[M]]() {
    self: M => // required to match Json.toJson signature

    implicit protected def fmt: OFormat[M]

    def toJson: JsValue = Json.toJson(this)
  }

  case class NotifyTableChange(tableStatus: BizTable) extends OutputMessage[NotifyTableChange] {

    override implicit protected def fmt: OFormat[NotifyTableChange] = Json.format[NotifyTableChange]
  }

显然你也想从 JSON 解码,你仍然需要一个伴生对象。


回复评论

  1. Referring the companion through a def - means that is a "method", thus defined once for all the instances of the subtype (and doesn't gets serialized)?

你用 val 声明的所有东西都会得到一个存储在对象中的字段(class 的实例)。默认情况下,序列化程序会尝试序列化所有字段。通常有一些方法可以说明某些字段应该被忽略(比如一些 @IgnoreAnnotation)。这也意味着您将在每个无缘无故使用内存的对象中多一个 pointer/reference,这对您来说可能是也可能不是问题。将其声明为 def 会获得一种方法,因此您可以只将一个对象存储在某些 "static" 地方,例如伴随对象,或者每次都按需构建它。

  1. I'm kinda new to Scala, and I've peeked up the habit to put the format inside the companion object, would you recommend/refer to some source, about how to decide where is best to put your methods?

Scala 是一种不寻常的语言,没有直接映射涵盖其他语言中 object 概念的所有用例。作为第一条经验法则,object 有两种主要用法:

  1. 你会在其他语言中使用 static 的东西,即静态方法、常量和静态变量的容器(尽管不鼓励使用变量,尤其是 Scala 中的静态变量)

  2. 单例模式的实现。

  1. By f-bound generic - do you mean the lower bound of the M being OutputMessage[M] (btw why is it ok using M twice in the same expr. ?)

遗憾的是,wiki 仅提供基本描述。 F-bounded 多态性的整个想法是能够以某种通用方式访问基 class 类型中子 class 的类型。通常 A <: B 约束意味着 A 应该是 B 的子类型。这里使用 M <: OutputMessage[M],这意味着 M 应该是 OutputMessage[M] 的子类型,只需声明子类型 class 即可轻松满足(还有其他非满足这一点的简单方法)如:

class Child extends OutputMessage[Child}

这种技巧允许您使用 M 作为方法中的参数或结果类型。

  1. I'm a bit puzzled about the self bit ...

最后,self 位是另一个必要的技巧,因为在这种特殊情况下,F 有界多态性是不够的。当特征用作 mix-in 时,它通常与 trait 一起使用。在这种情况下,您可能希望限制 class 可以混合特征的内容。并且在同一类型中,它允许您在混合中使用该基类型的方法 trait.

我会说我的回答中的特定用法有点不合常规,但它具有相同的双重效果:

  1. 在编译 OutputMessage 时,编译器可以假设该类型也将以某种方式成为 M 的类型(无论 M 是什么)

  2. 编译任何子类型时,编译器确保满足约束 #1。例如它不会让你做

case class SomeChild(i: Int) extends OutputMessage[SomeChild]

// this will fail because passing SomeChild breaks the restriction of self:M
case class AnotherChild(i: Int) extends OutputMessage[SomeChild]

实际上因为我不得不使用 self:M,你可能可以在这里删除 F-bounded 部分,只需要

abstract class OutputMessage[M]() {
    self: M =>
     ...
}

但我会保留它以更好地传达意思。

正如 SergGr 已经回答的那样,您现在需要一种 F-Bounded 多态性来解决这个问题。
但是,对于这些情况,我认为 (注意这只是我的意见) 最好使用 Typeclasses

在您的情况下,您只想为任何值提供 toJson 方法,只要它们具有 OFormat[T] class.
的实例 您可以使用这段 (更简单的恕我直言) 代码来实现。

object syntax {
  object json {
    implicit class JsonOps[T](val t: T) extends AnyVal {
      def toJson(implicit: fmt: OFormat[T]): JsVal = Json.toJson(t)(fmt)
    }
  }
}

final case class NotifyTableChange(tableStatus: BizTable)

object NotifyTableChange {
  implicit val fmt: OFormat[NotifyTableChange] = Json.format[NotifyTableChange]
}

import syntax.json._
val m = NotifyTableChange(tableStatus = ???)
val mJson = m.toJson // This works!

JsonOps class 是一个 Implicit Class ,它将为范围内存在隐式 OFormat 实例的任何值提供 toJson 方法.
并且由于 NotifyTableChange class 的 伴随对象 定义了这种隐式,它总是在范围内 - 有关 where does scala look for implicits in this link.[=36= 的更多信息] 此外,鉴于它是 Value Class,此扩展方法不需要在 runtime.

中进行任何实例化

Here,你可以找到关于 F-BoundedTypeclasses 的更详细讨论.