编译器说您的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 解码,你仍然需要一个伴生对象。
对cmets的回答
- 通过 def 引用同伴意味着这是一个“方法”,因此为子类型的所有实例定义一次(并且不会被序列化)?
您使用val 声明的所有内容都会获得一个存储在对象(类的实例)中的字段。默认情况下,序列化程序会尝试序列化所有字段。通常有一些方法可以说某些字段应该被忽略(比如一些@IgnoreAnnotation)。这也意味着您将在每个无缘无故地使用内存的对象中多了一个指针/引用,这对您来说可能是也可能不是问题。将其声明为 def 会获得一个方法,这样您就可以只将一个对象存储在某个“静态”位置,例如伴随对象,或者每次都按需构建它。
- 我对 Scala 有点陌生,而且我已经养成了将格式放在伴随对象中的习惯,你会推荐/参考一些来源,关于如何决定把你的方法放在哪里最好吗?
Scala 是一种不寻常的语言,没有直接映射覆盖object 概念在其他语言中的所有用例。作为第一条经验法则,object 有两个主要用途:
可以在其他语言中使用 static 的东西,即静态方法、常量和静态变量的容器(尽管不鼓励使用变量,尤其是 Scala 中的静态变量)
单例模式的实现。
- f-bound generic - 你的意思是 M 的下限是 OutputMessage[M](顺便说一句,为什么可以在同一个表达式中使用两次 M?)
不幸的是,wiki 只提供了一个基本的描述。 F 有界多态性的整个想法是能够以某种通用方式访问基类类型中的子类类型。通常A <: B 约束意味着A 应该是B 的子类型。这里有M <: OutputMessage[M],意味着M 应该是OutputMessage[M] 的子类型,只有通过声明子类(还有其他不易满足的方法)才能轻松满足:
class Child extends OutputMessage[Child}
这样的技巧允许您在方法中使用M 作为参数或结果类型。
- 我对self位有点疑惑……
最后,self 位是另一个必要的技巧,因为在这种特殊情况下 F 有界多态性是不够的。当特征用作mix-in 时,通常与trait 一起使用。在这种情况下,您可能希望限制可以在哪些类中混合 trait。并且在同一类型中,它允许您在 mixin trait 中使用该基类型的方法。
我会说我的答案中的特定用法有点不合常规,但它具有相同的双重效果:
在编译 OutputMessage 时,编译器可以假设该类型也将以某种方式属于 M(无论 M 是什么)
在编译任何子类型时,编译器确保满足约束 #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 =>
...
}
但我会坚持下去以更好地传达含义。