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


84

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

case class A(val s: String)

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

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

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


3
也许将标题更改为“如何覆盖案例类同伴中的应用”
ziggystar 2011年

1
如果糖不能满足您的需求,请不要使用糖
拉斐尔

7
@Raphael如果您想要红糖,即我们想要具有某些特殊属性的糖,该怎么办?额外申请。
StephenBoesch 2014年

仅供参考。此问题已在scala 2.12+中修复。在随播广告中定义其他方式的apply方法将阻止生成默认的apply方法。
stewSquared

Answers:


90

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

首先,我建议您使用require:

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

如果用户尝试创建s包含小写字符的实例,则将引发Exception。这是用例类的一种很好的用法,因为当您使用模式匹配(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


5
toCharArray电话是没有必要的,你也可以写s.exists(_.isLower)
Frank S. Thomas

4
顺便说一句,我认为s.forall(_.isUpper)比理解更容易!s.exists(_.isLower)
Frank S. Thomas

谢谢!这当然可以满足我的需求。@弗兰克,我同意s.forall(_isupper)更容易阅读。我将其与@olle的建议结合使用。
约翰S

4
“名称”“ case类”的+1表示应该可以使用提取(未修改的)构造函数参数match
Eugen Labun

2
@ollekullberg您不必离开使用案例类(而失去默认情况下案例案例类提供的所有其他好处)即可达到OP的预期效果。如果进行了两次修改,则可以拥有案例类,也可以使用它!A)将案例类标记为抽象,B)将案例类构造器标记为private [A](而不是私有)。使用此技术扩展案例类还存在其他一些更细微的问题。请参阅我发布的答案以获取更详细的信息:stackoverflow.com/a/25538287/501113
chaotic3quilibrium 2014年

28

更新2016/02/25:
尽管我在下面编写的答案仍然足够,但是还值得参考有关案例类的伴随对象的另一个相关答案。即,如何精确地重现编译器生成的隐式伴随对象,该对象仅在定义案例类本身时发生。对我而言,事实证明这与直觉相反。


摘要:
您可以更改案例类参数的值,然后再将其存储在案例类中,非常简单,同时仍保留有效的(已指定)ADT(抽象数据类型)。尽管解决方案相对简单,但是发现细节却更具挑战性。

细节:
如果要确保只能实例化case类的有效实例,这是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与您的case类的主构造函数具有完全相同的签名的方法
      • 一旦完成步骤2.1,它将成功编译
    3. 提供一个实现,该实现使用new运算符获取案例类的实例,并提供一个空的实现{}
      • 现在,这将严格按照您的条件实例化案例类
      • {}由于声明了案例类,因此必须提供空实现abstract(请参阅步骤2.1)。
  2. 对于您的案例类:
    1. 声明它 abstract
      • 防止Scala编译器apply在伴随对象中生成方法,而该方法正是导致“方法被定义两次...”的编译错误(上面的步骤1.2)
    2. 将主要构造函数标记为 private[A]
      • 现在,主要构造函数仅可用于case类本身及其伴随对象(我们在上面的步骤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 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)
}

尽管这直接回答了您的问题,但还有更多的方法可以在实例类之外扩展实例类之外的路径。为了满足我自己的项目需求,我创建了一个更扩展的解决方案,该解决方案在CodeReview(StackOverflow姐妹网站)上进行了记录。如果您最终查看,使用或利用我的解决方案,请考虑给我留下反馈,建议或问题,并在合理的范围内,我将尽力在一天之内做出答复。


我刚刚发布了一个新的扩展解决方案,以使Scala更加习惯化,并包括使用ScalaCache轻松缓存案例类实例(不允许根据元规则编辑现有答案):codereview.stackexchange.com/a/98367/4758
chaotic3quilibrium

感谢您的详细解释。但是,我很难理解为什么需要readResolve实现。因为在没有readResolve实现的情况下也可以进行编译。
mogli

发表了一个单独的问题:stackoverflow.com/questions/32236594/…–
mogli

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:是否可以覆盖默认的case类构造函数?


谢谢-我一直都在想,但是不知道Proxy!也许会更好s.toUpperCase 一次
本杰克逊

@Ben我看不到哪里toUpperCase叫过一次。
弗兰克·托马斯

你说得对val self,不是def self。我的脑子上只有C ++。
本杰克逊

6

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


1
这是什么意思?如何将这些知识应用于解决方案?你能举个例子吗?
k0pernikus

请注意,不应用不使用模式匹配case类,这使得它覆盖相当无用的(如果你-Xprintmatch言,你会看到,它不使用)。
J Cracknell

5

它适用于var变量:

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

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


4

在保留case类且没有隐式def或另一个构造函数的情况下,另一个想法是使签名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
chaotic3quilibrium 2014年

3

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

sealed trait A {
  def s:String
}

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

而且,如果需要任何方法,只需在trait中定义它并在case类中重写它即可。


0

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

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

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中编译并正常运行。


-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


3
如果将代码放入文件中并尝试对其进行编译,则此操作将不起作用。
Frank S. Thomas

我相信我在较早的答案中提出了类似的建议,并且有人说由于repl的工作方式,它仅在repl中有效。
本杰克逊

5
REPL实际上在前一行的每一行中都创建了一个新作用域。这就是为什么将某些东西从REPL粘贴到您的代码中时无法按预期工作的原因。因此,请务必同时检查两者。
gregturn

1
测试上述代码(不起作用)的正确方法是在REPL中使用:paste,以确保大小写和对象都一起定义。
StephenBoesch 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.