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

Joh*_*n S 81 scala pattern-matching case-class

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

case class A(val s: String)
Run Code Online (Sandbox Code Playgroud)

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

object A {
  def apply(s: String) = new A(s.toUpperCase)
}
Run Code Online (Sandbox Code Playgroud)

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

oll*_*erg 88

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

首先,我建议你使用require:

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

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

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

class A private (val s: String) {
}

object A {
  def apply(s: String): A = new A(s.toUpperCase)
}
Run Code Online (Sandbox Code Playgroud)

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

  • `toCharArray`调用不是必需的,你也可以编写`s.exists(_.isLower)`. (5认同)
  • 顺便说一下,我认为`s.forall(_.isUpper)`比`!s.exists(_.isLower)`更容易理解. (4认同)
  • "名称"案例类的+1表示应该可以使用`match`提取(未修改的)构造函数参数." (4认同)
  • @ollekullberg您不必放弃使用案例类(并且默认情况下会失去案例案例类提供的所有额外好处)以实现OP的预期效果.如果你做了两次修改,你可以让你的案例类也吃掉它!A)将case类标记为abstract,B)将case类构造函数标记为private [A](而不是private).使用这种技术扩展案例类还有一些其他更微妙的问题.有关详细信息,请参阅我发布的答案:http://stackoverflow.com/a/25538287/501113 (2认同)

cha*_*ium 26

更新2016/02/25:
虽然我在下面写的答案仍然足够,但值得引用关于案例类的伴侣对象的另一个相关答案.也就是说,如何精确地再现编译器生成的隐式伴随对象,该对象仅在定义案例类本身时发生.对我来说,事实证明这是违反直觉的.


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

详细信息:
如果要确保只能实例化案例类的有效实例,这是ADT(抽象数据类型)背后的基本假设,那么您必须执行许多操作.

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

val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"
Run Code Online (Sandbox Code Playgroud)

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

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

  1. 对于您的显式伴随对象:
    1. 使用与案例类完全相同的名称创建它
      • 这可以访问案例类的私有部分
    2. 创建一个apply方法,其签名与案例类的主要构造函数完全相同
      • 一旦步骤2.1完成,这将成功编译
    3. 提供使用new运算符获取案例类实例并提供空实现的实现{}
      • 现在,这将严格按照您的条款实例化案例类
      • {}必须提供空实现,因为声明了case类abstract(参见步骤2.1)
  2. 对于您的案例类:
    1. 宣布它 abstract
      • 阻止Scala编译器apply在随播对象中生成一个方法,该方法导致"方法定义两次......"编译错误(上面的步骤1.2)
    2. 将主构造函数标记为 private[A]
      • 主构造函数现在仅可用于案例类本身及其伴随对象(我们在上面的步骤1.1中定义的对象)
    3. 创建一个readResolve方法
      1. 使用apply方法提供实现(上面的步骤1.2)
    4. 创建一个copy方法
      1. 将其定义为与case类的主构造函数具有完全相同的签名
      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)
}
Run Code Online (Sandbox Code Playgroud)

在实现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)
}
Run Code Online (Sandbox Code Playgroud)

如果此代码将通过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)
}
Run Code Online (Sandbox Code Playgroud)

虽然这直接回答了您的问题,但除了实例缓存之外,还有更多方法可以围绕案例类扩展此路径.对于我自己的项目需求,我创建了一个更广泛的解决方案,我已经在CodeReview(StackOverflow姐妹站点)上记录了这个解决方案.如果您最终查看,使用或利用我的解决方案,请考虑留下反馈,建议或问题,并在合理范围内,我会尽力在一天内回复.


Fra*_*mas 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"))
Run Code Online (Sandbox Code Playgroud)

以上代码输出:

A(HELLO)
Run Code Online (Sandbox Code Playgroud)

您还应该看一下这个问题及其答案:Scala:是否可以覆盖默认的case类构造函数?


小智 6

对于2017年4月之后阅读此内容的人:从Scala 2.12.2+开始,Scala 允许覆盖默认情况下的apply和 unapply .您也可以通过-Xsource:2.12在Scala 2.11.11+上为编译器提供选项来获得此行为.


Mik*_*yer 5

它适用于 var 变量:

case class A(var s: String) {
   // Conversion
   s = s.toUpperCase
}
Run Code Online (Sandbox Code Playgroud)

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