【问题标题】:How to override apply in a case class companion如何在案例类同伴中覆盖应用
【发布时间】:2011-08-15 05:05:09
【问题描述】:

所以情况就是这样。我想像这样定义一个案例类:

case class A(val s: String)

并且我想定义一个对象,以确保在创建类的实例时,'s' 的值始终为大写,如下所示:

object A {
  def apply(s: String) = new A(s.toUpperCase)
}

但是,这不起作用,因为 Scala 抱怨 apply(s: String) 方法被定义了两次。我知道案例类语法会自动为我定义它,但是我没有其他方法可以实现这一点吗?我想坚持使用 case 类,因为我想将它用于模式匹配。

【问题讨论】:

  • 也许将标题更改为“如何在案例类同伴中覆盖应用”
  • 如果糖不能满足您的要求,请不要使用...
  • @Raphael 如果你想要红糖怎么办,即我们想要具有一些特殊属性的糖。我有与 OP 完全相同的查询:案例类很有用,但它是一个足够常见的用例想要用额外的应用来装饰伴随对象。
  • 仅供参考 这在 scala 2.12+ 中已修复。在伴随程序中定义否则会产生冲突的 apply 方法会阻止生成默认的 apply 方法。

标签: scala pattern-matching case-class


【解决方案1】:

冲突的原因是case类提供了完全相同的apply()方法(相同的签名)。

首先我想建议你使用require:

case class A(s: String) {
  require(! s.toCharArray.exists( _.isLower ), "Bad string: "+ s)
}

如果用户尝试创建 s 包含小写字符的实例,这将引发异常。这是对案例类的一个很好的使用,因为您放入构造函数的内容也是您在使用模式匹配时得到的内容 (match)。

如果这不是你想要的,那么我会让构造函数 private 并强制用户使用 apply 方法:

class A private (val s: String) {
}

object A {
  def apply(s: String): A = new A(s.toUpperCase)
}

如您所见,A 不再是 case class。我不确定具有不可变字段的案例类是否用于修改传入值,因为名称“案例类”意味着应该可以使用match 提取(未修改的)构造函数参数。

【讨论】:

  • toCharArray 调用不是必须的,你也可以写成s.exists(_.isLower)
  • 顺便说一句,我认为s.forall(_.isUpper)!s.exists(_.isLower) 更容易理解。
  • 谢谢!这当然适合我的需要。 @Frank,我同意 s.forall(_isupper) 更容易阅读。我将结合@olle 的建议使用它。
  • +1 表示“名称“案例类”意味着应该可以使用match 提取(未修改的)构造函数参数。”
  • @ollekullberg 您不必放弃使用案例类(并失去案例类默认提供的所有额外好处)来实现 OP 的预期效果。如果您进行两次修改,您就可以拥有您的案例课程并吃掉它! A)将案例类标记为抽象,B)将案例类构造函数标记为私有[A](而不是仅仅私有)。使用这种技术扩展案例类还有一些其他更微妙的问题。有关详细信息,请参阅我发布的答案:stackoverflow.com/a/25538287/501113
【解决方案2】:

2016 年 2 月 25 日更新:
虽然我在下面写的答案仍然足够,但也值得参考关于案例类的伴随对象的另一个相关答案。即,how does one exactly reproduce the compiler generated implicit companion object 仅在定义案例类本身时发生。对我来说,结果证明是反直觉的。


总结:
您可以在将案例类参数的值存储在案例类中之前非常简单地更改它的值,同时它仍然是有效的(经过验证的)ADT(抽象数据类型)。虽然解决方案相对简单,但发现细节更具挑战性。

详情:
如果您想确保只能实例化您的案例类的有效实例,这是 ADT(抽象数据类型)背后的基本假设,您必须做很多事情。

例如,编译器生成的copy 方法默认提供在案例类上。因此,即使您非常小心地确保仅通过显式伴随对象的 apply 方法创建实例,该方法保证它们只能包含大写值,以下代码仍会生成具有小写值的案例类实例: /p>

val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"

此外,案例类实现java.io.Serializable。这意味着您可以使用简单的文本编辑器和反序列化来颠覆您仅使用大写实例的谨慎策略。

因此,对于可以使用案例类的所有各种方式(善意和/或恶意),以下是您必须采取的行动:

  1. 对于显式伴随对象:
    1. 使用与案例类完全相同的名称创建它
      • 这可以访问案例类的私有部分
    2. 创建一个apply 方法,其签名与案例类的主构造函数完全相同
      • 一旦步骤 2.1 完成,这将成功编译
    3. 提供使用new运算符获取案例类实例的实现,并提供空实现{}
      • 现在这将严格按照您的条件实例化案例类
      • 必须提供空实现{},因为案例类声明为abstract(参见步骤2.1)
  2. 对于您的案例类:
    1. 声明abstract
      • 防止 Scala 编译器在伴随对象中生成 apply 方法,这是导致“方法定义两次...”编译错误的原因(上面的步骤 1.2)
    2. 将主构造函数标记为private[A]
      • 主构造函数现在仅可用于案例类本身及其伴生对象(我们在上面的步骤 1.1 中定义的对象)
    3. 创建readResolve 方法
      1. 使用 apply 方法提供一个实现(上面的步骤 1.2)
    4. 创建copy 方法
      1. 将其定义为与案例类的主构造函数具有完全相同的签名
      2. 为每个参数添加一个使用相同参数名称的默认值(例如:s: String = s
      3. 使用 apply 方法提供一个实现(下面的步骤 1.2)

这是您使用上述操作修改的代码:

object A {
  def apply(s: String, i: Int): A =
    new A(s.toUpperCase, i) {} //abstract class implementation intentionally empty
}
abstract case class A private[A] (s: String, i: Int) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

这是您在实现 require 后的代码(在@ollekullberg 答案中建议),并且还确定了放置任何类型缓存的理想位置:

object A {
  def apply(s: String, i: Int): A = {
    require(s.forall(_.isUpper), s"Bad String: $s")
    //TODO: Insert normal instance caching mechanism here
    new A(s, i) {} //abstract class implementation intentionally empty
  }
}
abstract case class A private[A] (s: String, i: Int) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

如果此代码将通过 Java 互操作使用(隐藏案例类作为实现并创建一个防止派生的最终类),则此版本更安全/更健壮:

object A {
  private[A] abstract case class AImpl private[A] (s: String, i: Int)
  def apply(s: String, i: Int): A = {
    require(s.forall(_.isUpper), s"Bad String: $s")
    //TODO: Insert normal instance caching mechanism here
    new A(s, i)
  }
}
final class A private[A] (s: String, i: Int) extends A.AImpl(s, i) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

虽然这直接回答了您的问题,但除了实例缓存之外,还有更多方法可以围绕案例类扩展此路径。对于我自己的项目需求,我有created an even more expansive solution,我有documented on CodeReview(StackOverflow 姊妹网站)。如果您最终查看、使用或利用我的解决方案,请考虑给我留下反馈、建议或问题,并且在合理范围内,我会尽我所能在一天内回复。

【讨论】:

  • 我刚刚发布了一个更新的扩展解决方案,使其更加符合 Scala 习惯,并包括使用 ScalaCache 轻松缓存案例类实例(不允许根据元规则编辑现有答案):codereview.stackexchange.com/a/98367/4758
  • 感谢您的详细解释。但是,我很难理解,为什么需要 readResolve 实现。因为编译也可以在没有 readResolve 实现的情况下工作。
  • 发表了一个单独的问题:stackoverflow.com/questions/32236594/…
【解决方案3】:

我不知道如何覆盖伴随对象中的 apply 方法(如果可能的话),但您也可以为大写字符串使用特殊类型:

class UpperCaseString(s: String) extends Proxy {
  val self: String = s.toUpperCase
}

implicit def stringToUpperCaseString(s: String) = new UpperCaseString(s)
implicit def upperCaseStringToString(s: UpperCaseString) = s.self

case class A(val s: UpperCaseString)

println(A("hello"))

以上代码输出:

A(HELLO)

您还应该看看这个问题及其答案:Scala: is it possible to override default case class constructor?

【讨论】:

  • 谢谢你——我也有同样的想法,但不知道Proxys.toUpperCase once 可能会更好。
  • @Ben 我没有看到toUpperCase 被多次调用。
  • 你说的很对,val self,而不是def self。我刚开始学习 C++。
【解决方案4】:

对于 2017 年 4 月之后阅读本文的人:从 Scala 2.12.2+ 开始,Scala allows overriding apply and unapply by default。您也可以通过为 Scala 2.11.11+ 上的编译器提供 -Xsource:2.12 选项来获得此行为。

【讨论】:

  • 这是什么意思?我如何将这些知识应用到解决方案中?你能举个例子吗?
  • 请注意,unapply 不用于模式匹配案例类,这使得覆盖它相当无用(如果你 -Xprint 一个 match 语句,你会看到它没有被使用)。
【解决方案5】:

它适用于 var 变量:

case class A(var s: String) {
   // Conversion
   s = s.toUpperCase
}

这种做法显然是在案例类中鼓励的,而不是定义另一个构造函数。 See here.。复制对象时,您也保留相同的修改。

【讨论】:

    【解决方案6】:

    在保持 case class 并且没有隐式 defs 或其他构造函数的同时,另一个想法是使 apply 的签名略有不同,但从用户的角度来看是相同的。 我在某个地方看到了隐含的技巧,但不记得/找到它是哪个隐含参数,所以我在这里选择了Boolean。如果有人能帮我完成这个把戏......

    object A {
      def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
    }
    case class A(s: String)
    

    【讨论】:

    • 在调用站点它会给你一个编译错误(对重载定义的模糊引用)。它仅在 scala 类型不同但擦除后相同时才有效,例如为 List[Int] 和 List[String] 提供两个不同的函数。
    • 我无法使用此解决方案途径(使用 2.11)。我终于弄清楚了为什么他不能在显式伴随对象上提供自己的 apply 方法。我在刚刚发布的答案中已经详细说明了:stackoverflow.com/a/25538287/501113
    【解决方案7】:

    我遇到了同样的问题,这个解决方案对我来说没问题:

    sealed trait A {
      def s:String
    }
    
    object A {
      private case class AImpl(s:String)
      def apply(s:String):A = AImpl(s.toUpperCase)
    }
    

    而且,如果需要任何方法,只需在 trait 中定义它并在案例类中覆盖它。

    【讨论】:

      【解决方案8】:

      如果您遇到了默认情况下无法覆盖的旧 scala,或者您不想像 @mehmet-emre 显示的那样添加编译器标志,并且您需要一个案例类,您可以执行以下操作:

      case class A(private val _s: String) {
        val s = _s.toUpperCase
      }
      

      【讨论】:

        【解决方案9】:

        截至 2020 年的 Scala 2.13,上述使用相同签名覆盖案例类应用方法的场景完全正常。

        case class A(val s: String)
        
        object A {
          def apply(s: String) = new A(s.toUpperCase)
        }
        

        上面的 sn-p 在 Scala 2.13 中在 REPL 和非 REPL 模式下编译和运行都很好。

        【讨论】:

          【解决方案10】:

          我认为这完全符合您的要求。这是我的 REPL 会话:

          scala> case class A(val s: String)
          defined class A
          
          scala> object A {
               | def apply(s: String) = new A(s.toUpperCase)
               | }
          defined module A
          
          scala> A("hello")
          res0: A = A(HELLO)
          

          这是使用 Scala 2.8.1.final

          【讨论】:

          • 这里如果我把代码放到一个文件中并尝试编译它就行不通了。
          • 我相信我在较早的答案中提出了类似的建议,有人说由于 repl 的工作方式,它只在 repl 中有效。
          • REPL 本质上为每一行创建了一个新的范围,在前一个范围内。这就是为什么当从 REPL 粘贴到您的代码中时,有些东西不能按预期工作的原因。因此,请始终检查两者。
          • 测试上述代码(不起作用)的正确方法是在 REPL 中使用 :paste 以确保同时定义大小写和对象。
          猜你喜欢
          • 1970-01-01
          • 2015-02-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-12-15
          • 1970-01-01
          • 1970-01-01
          • 2015-11-13
          相关资源
          最近更新 更多