如何在案例类同伴中覆盖 apply

发布于 2024-11-04 16:29:07 字数 328 浏览 6 评论 0原文

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

case class A(val s: String)

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

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

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

So here's the situation. I want to define a case class like so:

case class A(val s: String)

and I want to define an object to ensure that when I create instances of the class, the value for 's' is always uppercase, like so:

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

However, this doesn't work since Scala is complaining that the apply(s: String) method is defined twice. I understand that the case class syntax will automatically define it for me, but isn't there another way I can achieve this? I'd like to stick with the case class since I want to use it for pattern matching.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

郁金香雨 2024-11-11 16:29:07

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

首先,我建议您使用 require:

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

如果用户尝试创建 s 包含小写字符的实例,这将引发异常。这是案例类的一个很好的用途,因为您放入构造函数中的内容也是您在使用模式匹配 (match) 时得到的内容。

如果这不是您想要的,那么我会将构造函数设置为私有,并强制用户仅使用 apply 方法:

class A private (val s: String) {
}

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

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

The reason for the conflict is that the case class provides the exact same apply() method (same signature).

First of all I would like to suggest you use require:

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

This will throw an Exception if the user tries to create an instance where s includes lower case chars. This is a good use of case classes, since what you put into the constructor also is what you get out when you use pattern matching (match).

If this is not what you want, then I would make the constructor private and force the users to only use the apply method:

class A private (val s: String) {
}

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

As you see, A is no longer a case class. I am not sure if case classes with immutable fields are meant for modification of the incoming values, since the name "case class" implies it should be possible to extract the (unmodified) constructor arguments using match.

深巷少女 2024-11-11 16:29:07

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.Serialized。这意味着您只使用大写实例的谨慎策略可以通过简单的文本编辑器和反序列化来破坏。

因此,对于您的案例类可以使用的所有各种方式(善意和/或恶意),以下是您必须采取的操作:

  1. 对于您的显式伴生对象:
    1. 使用与您的案例类完全相同的名称来创建它
      • 它可以访问案例类的私有部分
    2. 创建一个 apply 方法,其签名与案例类的主构造函数完全相同
      • 完成步骤 2.1 后将成功编译
    3. 提供一个实现,使用 new 运算符获取案例类的实例并提供一个空实现{}
      • 现在将严格按照您的条件实例化案例类
      • 必须提供空实现 {},因为案例类被声明为 abstract(请参阅步骤 2.1)
  2. )您的案例类别:
    1. 声明它抽象
      • 防止 Scala 编译器在伴生对象中生成 apply 方法,这会导致“方法被定义两次...”编译错误(上面的步骤 1.2)
    2. 将主构造函数标记为 private[A]
      • 主构造函数现在仅适用于案例类本身及其伴生对象(我们在上面的步骤 1.1 中定义的对象)
    3. 创建一个readResolve方法
      1. 使用 apply 方法提供实现(上面的步骤 1.2)
    4. 创建一个复制方法
      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 姊妹网站)上。如果您最终查看、使用或利用我的解决方案,请考虑给我留下反馈、建议或问题,在合理范围内,我将尽力在一天内回复。

UPDATE 2016/02/25:
While the answer I wrote below remains sufficient, it's worth also referencing another related answer to this regarding the case class's companion object. Namely, how does one exactly reproduce the compiler generated implicit companion object which occurs when one only defines the case class itself. For me, it turned out to be counter intuitive.


Summary:
You can alter the value of a case class parameter before it is stored in the case class pretty simply while it still remaining a valid(ated) ADT (Abstract Data Type). While the solution was relatively simple, discovering the details was quite a bit more challenging.

Details:
If you want to ensure only valid instances of your case class can ever be instantiated which is an essential assumption behind an ADT (Abstract Data Type), there are a number of things you must do.

For example, a compiler generated copy method is provided by default on a case class. So, even if you were very careful to ensure only instances were created via the explicit companion object's apply method which guaranteed they could only ever contain upper case values, the following code would produce a case class instance with a lower case value:

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

Additionally, case classes implement java.io.Serializable. This means that your careful strategy to only have upper case instances can be subverted with a simple text editor and deserialization.

So, for all the various ways your case class can be used (benevolently and/or malevolently), here are the actions you must take:

  1. For your explicit companion object:
    1. Create it using exactly the same name as your case class
      • This has access to the case class's private parts
    2. Create an apply method with exactly the same signature as the primary constructor for your case class
      • This will successfully compile once step 2.1 is completed
    3. Provide an implementation obtaining an instance of the case class using the new operator and providing an empty implementation {}
      • This will now instantiate the case class strictly on your terms
      • The empty implementation {} must be provided because the case class is declared abstract (see step 2.1)
  2. For your case class:
    1. Declare it abstract
      • Prevents the Scala compiler from generating an apply method in the companion object which is what was causing the "method is defined twice..." compilation error (step 1.2 above)
    2. Mark the primary constructor as private[A]
      • The primary constructor is now only available to the case class itself and to its companion object (the one we defined above in step 1.1)
    3. Create a readResolve method
      1. Provide an implementation using the apply method (step 1.2 above)
    4. Create a copy method
      1. Define it to have exactly the same signature as the case class's primary constructor
      2. For each parameter, add a default value using the same parameter name (ex: s: String = s)
      3. Provide an implementation using the apply method (step 1.2 below)

Here's your code modified with the above actions:

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

And here's your code after implementing the require (suggested in the @ollekullberg answer) and also identifying the ideal place to put any sort of caching:

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

And this version is more secure/robust if this code will be used via Java interop (hides the case class as an implementation and creates a final class which prevents derivations):

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

While this directly answers your question, there are even more ways to expand this pathway around case classes beyond instance caching. For my own project needs, I have created an even more expansive solution which I have documented on CodeReview (a StackOverflow sister site). If you end up looking it over, using or leveraging my solution, please consider leaving me feedback, suggestions or questions and within reason, I will do my best to respond within a day.

木格 2024-11-11 16:29:07

我不知道如何重写伴随对象中的 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?

I don't know how to override the apply method in the companion object (if that is even possible) but you could also use a special type for upper case strings:

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

The above code outputs:

A(HELLO)

You should also have a look at this question and it's answers: Scala: is it possible to override default case class constructor?

〃安静 2024-11-11 16:29:07

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

For the people reading this after April 2017: As of Scala 2.12.2+, Scala allows overriding apply and unapply by default. You can get this behavior by giving -Xsource:2.12 option to the compiler on Scala 2.11.11+ as well.

花开雨落又逢春i 2024-11-11 16:29:07

它适用于 var 变量:

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

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

It works with var variables:

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

This practice is apparently encouraged in case classes instead of defining another constructor. See here.. When copying an object, you also keep the same modifications.

メ斷腸人バ 2024-11-11 16:29:07

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

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

Another idea while keeping case class and having no implicit defs or another constructor is to make the signature of apply slightly different but from a user perspective the same.
Somewhere I have seen the implicit trick, but can´t remember/find which implicit argument it was, so I chose Boolean here. If someone can help me out and finish the trick...

object A {
  def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
}
case class A(s: String)
是你 2024-11-11 16:29:07

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

sealed trait A {
  def s:String
}

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

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

I faced the same problem and this solution is ok for me:

sealed trait A {
  def s:String
}

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

And, if any method is needed, just define it in the trait and override it in the case class.

情感失落者 2024-11-11 16:29:07

如果你坚持使用旧版 scala,默认情况下无法覆盖,或者你不想添加编译器标志(如 @mehmet-emre 所示),并且你需要一个案例类,你可以执行以下操作:

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

If you're stuck with older scala where you cant override by default or you dont want to add the compiler flag as @mehmet-emre showed, and you require a case class, you can do the following:

case class A(private val _s: String) {
  val s = _s.toUpperCase
}
離殇 2024-11-11 16:29:07

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

case class A(val s: String)

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

上面的代码片段在 Scala 2.13 中可以在 REPL 和 REPL 中编译并运行得很好。非 REPL 模式。

As of 2020 on Scala 2.13, the above scenario of overriding a case class apply method with same signature works totally fine.

case class A(val s: String)

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

the above snippet compiles and runs just fine in Scala 2.13 both in REPL & non-REPL modes.

惯饮孤独 2024-11-11 16:29:07

我认为这已经完全按照你想要的方式工作了。这是我的 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

I think this works exactly how you want it to already. Here's my REPL session:

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)

This is using Scala 2.8.1.final

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文