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

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

case class A(val s: String) 

我想定义一个对象来确保当我创build类的实例时,“s”的值总是大写,如下所示:

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

然而,这是行不通的,因为Scala抱怨apply(s:String)方法被定义了两次。 我明白,案例类语法会自动为我定义,但不是有另一种方式,我可以实现这一目标吗? 我想坚持案例类,因为我想用它来进行模式匹配。

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

首先我想build议你使用require:

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

如果用户试图创build一个包含小写字符的实例,这将会抛出exception。 这是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来提取(未修改的)构造函数参数。

更新2016/02/25:
虽然我在下面写的答案仍然充足,但值得引用关于case类的伴随对象的另一个相关答案。 也就是说, 如何正确地重现编译器生成的隐式伴侣对象 ,当只定义案例类本身时就会发生。 对我来说,事实certificate是违反直觉的。


概要:
您可以在case类的参数存储在case类中之前修改case类的参数值,只需保留一个有效的(有效的)ADT(抽象数据types)即可。 虽然解决scheme相对简单,但发现细节却相当具有挑战性。

细节:
如果你想确保只有你的case类的有效实例能被实例化,这是ADT(抽象数据types)背后的一个基本假设,那么你需要做很多事情。

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

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

另外,case类实现java.io.Serializable 。 这意味着只有大写实例的谨慎策略才能被简单的文本编辑器和反序列化所颠覆。

所以,对于所有不同的方式你的案例类可以使用(仁慈和/或恶意),这里是你必须采取的行动:

  1. 对于您的显式伴侣对象:
    1. 使用与您的案例类完全相同的名称创build它
      • 这可以访问案例类的私人部分
    2. 创build与您的案例类的主构造函数完全相同的签名的apply方法
      • 一旦步骤2.1完成,这将成功编译
    3. 提供一个使用new运算符获得case类实例的实现,并提供一个空的实现{}
      • 现在将严格按照您的条款实例化案例类
      • 必须提供空实现{} ,因为case类被声明为abstract (参见步骤2.1)
  2. 对于你的情况类:
    1. 声明它是abstract
      • 防止Scala编译器在伴随对象中生成apply方法,这是导致“方法定义两次…”编译错误(上面的步骤1.2)
    2. 将主构造器标记为private[A]
      • 主构造函数现在只对case类本身和其伴侣对象(我们在步骤1.1中定义的那个)
    3. 创build一个readResolve方法
      1. 使用apply方法提供一个实现(上面的步骤1.2)
    4. 创build一个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答案中build议)之后,这里是你的代码,同时也确定了放置任何typescaching的理想位置:

 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类作为一个实现并创build一个阻止派生的最终类),那么这个版本更安全可靠。

 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) } 

虽然这直接回答了你的问题,但是还有更多的方法来扩展这个实例caching以外的case类。 对于我自己的项目需求,我创build了一个更加广泛的解决scheme ,我已经在CodeReview (一个StackOverflow姊妹站点) 上logging了这个解决scheme 。 如果你最终看到它,使用或利用我的解决scheme,请考虑留下我的反馈意见,build议或问题,并在合理的范围内,我会尽我所能在一天内作出回应。

我不知道如何重写伴随对象中的apply方法(如果这是可能的话),但是你也可以使用一个特殊types的大写string:

 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:是否有可能重写默认的case类构造函数?

保留大小写types并且没有隐含的defs或者其他构造函数的另一个想法是使得应用的签名略微不同,但是从用户angular度来看是相同的。 在某处,我看到了隐含的诡计,但是不能记住/发现它是什么隐含的论证,所以我在这里select了Boolean 。 如果有人能帮我解决问题

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

它适用于varvariables:

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

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

我面临同样的问题,这个解决scheme对我来说可以:

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

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

我认为这就是你想要的。 这是我的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