86

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

case class A(val s: String)

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

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

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

4

10 回答 10

90

冲突的原因是案例类提供了完全相同的 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.

于 2011-04-29T06:54:08.817 回答
28

更新 2016/02/25:
虽然我在下面写的答案仍然足够,但也值得参考关于案例类的伴随对象的另一个相关答案。即,如何准确地重现编译器生成的隐式伴随对象,这种对象仅在定义案例类本身时发生。对我来说,结果证明是反直觉的。


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

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

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

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

虽然这直接回答了您的问题,但除了实例缓存之外,还有更多方法可以围绕案例类扩展此路径。为了我自己的项目需求,我创建了一个更广泛的解决方案,我在 CodeReview(StackOverflow 姊妹网站)上记录了该解决方案。如果您最终查看、使用或利用我的解决方案,请考虑给我留下反馈、建议或问题,并且在合理范围内,我将尽我所能在一天内做出回应。

于 2014-08-27T23:04:53.970 回答
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?

于 2011-04-29T06:25:09.063 回答
7

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

于 2018-02-05T07:44:22.370 回答
5

它适用于 var 变量:

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

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

于 2013-11-28T10:10:42.633 回答
4

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

object A {
  def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
}
case class A(s: String)
于 2011-04-29T07:05:56.470 回答
3

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

sealed trait A {
  def s:String
}

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

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

于 2016-01-21T15:15:14.913 回答
0

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

case class A(private val _s: String) {
  val s = _s.toUpperCase
}
于 2018-06-25T15:44:34.653 回答
0

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

case class A(val s: String)

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

上面的代码片段在 Scala 2.13 中在 REPL 和非 REPL 模式下编译和运行都很好。

于 2020-10-08T08:03:38.000 回答
-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

于 2011-04-29T04:34:02.233 回答