如何覆盖案例类伴随中的应用

时间:2011-04-29 03:25:51

标签: scala pattern-matching case-class

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

case class A(val s: String)

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

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

但是,这不起作用,因为Scala抱怨apply(s:String)方法定义了两次。我理解case类语法会自动为我定义它,但是我不能用另一种方法来实现它吗?我想坚持使用case类,因为我想将它用于模式匹配。

10 个答案:

答案 0 :(得分:85)

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

首先,我建议你使用require:

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

如果用户尝试创建s包含小写字符的实例,则会抛出异常。这是一个很好用的case类,因为你在构造函数中添加的内容也是你在使用模式匹配时得到的结果(match)。

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

class A private (val s: String) {
}

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

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

答案 1 :(得分:25)

更新2016/02/25:
虽然我在下面写的答案仍然足够,但值得引用关于案例类的伴侣对象的另一个相关答案。即,how does one exactly reproduce the compiler generated implicit companion object当一个人只定义案例类本身时发生。对我来说,事实证明这是违反直觉的。


<强>要点:
您可以更改case类参数的值,然后将其保存在case类中,同时它仍然是有效的(ated)ADT(抽象数据类型)。虽然解决方案相对简单,但发现细节却更具挑战性。

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

例如,默认情况下,在案例类中提供了编译器生成的copy方法。因此,即使您非常小心地确保只通过显式伴随对象apply方法创建了实例,这些方法保证它们只能包含大写值,下面的代码将生成一个case类实例。小写值:

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方法,这导致&#34;方法被定义两次...&#34;编译错误(上面的步骤1.2)
    2. 将主要构造函数标记为private[A]
      • 主构造函数现在仅可用于案例类本身及其伴随对象(我们在上面的步骤1.1中定义的那个)
    3. 创建readResolve方法
      1. 使用apply方法提供实现(上面的步骤1.2)
    4. 创建copy方法
      1. 将其定义为与案例类的主要构造函数具有完全相同的签名
      2. 对于每个参数,使用相同的参数名称添加默认值(例如:s: String = s
      3. 使用apply方法提供实现(下面的步骤1.2)
  3. 通过以上操作修改了您的代码:

    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 interop使用,则此版本更安全/更健壮(将case类隐藏为实现并创建一个阻止派生的最终类):

    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姐妹网站)。如果您最终查看,使用或利用我的解决方案,请考虑留下反馈,建议或问题,并在合理范围内,我会尽力在一天内回复。

答案 2 :(得分:12)

我不知道如何覆盖随播对象中的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?

答案 3 :(得分:6)

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

答案 4 :(得分:4)

保留案例类并且没有隐式defs或其他构造函数的另一个想法是使apply的签名略有不同,但从用户角度来看也是如此。 在某个地方,我看到了隐含的技巧,但不记得/找到它的隐含参数,所以我在这里选择了Boolean。如果有人可以帮我解决问题......

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

答案 5 :(得分:4)

它适用于var变量:

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

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

答案 6 :(得分:2)

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

sealed trait A {
  def s:String
}

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

并且,如果需要任何方法,只需在特征中定义它并在case类中覆盖它。

答案 7 :(得分:0)

如果您受困于旧的scala,在默认情况下您无法覆盖它,或者您不想添加显示为@ mehmet-emre的编译器标志,并且需要使用case类,则可以执行以下操作:

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

答案 8 :(得分:0)

截至2020年,在Scala 2.13上,以上覆盖具有相同签名的案例类套用方法的方案完全可以正常工作。

case class A(val s: String)

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

以上代码段在REPL和non-REPL模式下均可在Scala 2.13中编译并正常运行。

答案 9 :(得分:-2)

我认为这完全符合您的要求。这是我的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