【问题标题】:Passing generic companion object to super constructor将通用伴随对象传递给超级构造函数
【发布时间】:2019-01-21 16:01:28
【问题描述】:

我正在尝试构造一个 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

【问题讨论】:

    标签: json scala generics companion-object


    【解决方案1】:

    编译器说您的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的回答

    1. 通过 def 引用同伴意味着这是一个“方法”,因此为子类型的所有实例定义一次(并且不会被序列化)?

    您使用val 声明的所有内容都会获得一个存储在对象(类的实例)中的字段。默认情况下,序列化程序会尝试序列化所有字段。通常有一些方法可以说某些字段应该被忽略(比如一些@IgnoreAnnotation)。这也意味着您将在每个无缘无故地使用内存的对象中多了一个指针/引用,这对您来说可能是也可能不是问题。将其声明为 def 会获得一个方法,这样您就可以只将一个对象存储在某个“静态”位置,例如伴随对象,或者每次都按需构建它。

    1. 我对 Scala 有点陌生,而且我已经养成了将格式放在伴随对象中的习惯,你会推荐/参考一些来源,关于如何决定把你的方法放在哪里最好吗?

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

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

    2. 单例模式的实现。

    1. f-bound generic - 你的意思是 M 的下限是 OutputMessage[M](顺便说一句,为什么可以在同一个表达式中使用两次 M?)

    不幸的是,wiki 只提供了一个基本的描述。 F 有界多态性的整个想法是能够以某种通用方式访问基类类型中的子类类型。通常A &lt;: B 约束意味着A 应该是B 的子类型。这里有M &lt;: OutputMessage[M],意味着M 应该是OutputMessage[M] 的子类型,只有通过声明子类(还有其他不易满足的方法)才能轻松满足:

    class Child extends OutputMessage[Child}
    

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

    1. 我对self位有点疑惑……

    最后,self 位是另一个必要的技巧,因为在这种特殊情况下 F 有界多态性是不够的。当特征用作mix-in 时,通常与trait 一起使用。在这种情况下,您可能希望限制可以在哪些类中混合 trait。并且在同一类型中,它允许您在 mixin 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 =>
         ...
    }
    

    但我会坚持下去以更好地传达含义。

    【讨论】:

    • 我认为您应该将self: M =&gt; 添加到 OutputMessage 以确保您提供正确的通用和正确的伴侣。
    • @AlekseyIsachenkov,不错的收获。在Json.toJson(this)调用时实际上需要满足编译器
    • 几个问题只是为了澄清: 1. 通过 def 引用同伴 - 意味着这是一种“方法”,因此为子类型的所有实例定义一次(并且不会被序列化) ? 2. 我对 Scala 有点陌生,而且我已经养成了将格式放在伴随对象中的习惯,你会推荐/参考一些来源,关于如何决定把你的方法放在哪里最好吗? 3. f-bound generic - 你的意思是 M 的下限是 OutputMessage[M] (顺便说一句,为什么可以在同一个表达式中使用 M 两次?) 4. 我对 self 位有点困惑...
    • @Lev,我试图在更新的答案中解决您的问题,
    • @Lev,感谢您接受它,但我建议您也看看Luis'es answer,并考虑这两种方法的优缺点。
    【解决方案2】:

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

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

    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 类是一个 Implicit Class,它将为任何在范围内存在隐式 OFormat 实例的值提供 toJson 方法。
    由于NotifyTableChange 类的伴生对象 定义了这样的隐式,它始终在范围内 - 有关where does scala look for implicits in this link 的更多信息。
    此外,鉴于它是 Value Class,此扩展方法不需要在 runtime 中进行任何实例化。

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

    【讨论】:

    • 这个解决方案很棒!我将阅读提供的链接 =)
    • 这里只需要稍微调整一下:Json.toJson(this)(fmt) -> Json.toJson(t)(fmt)
    • @Lev 很好,谢谢! 抱歉,如果这给您带来了问题,我复制并粘贴了,忘记更改了。答案已更新。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-03
    • 2014-06-04
    • 2018-09-12
    • 2019-03-18
    相关资源
    最近更新 更多