ChatGPT解决这个技术问题 Extra ChatGPT

声明 Scala 案例类有什么缺点?

如果您正在编写使用大量漂亮、不可变数据结构的代码,案例类似乎是天赐之物,只需一个关键字即可免费为您提供以下所有功能:

默认情况下一切都是不可变的

自动定义的吸气剂

体面的 toString() 实现

符合 equals() 和 hashCode()

带有用于匹配的 unapply() 方法的伴侣对象

但是将不可变数据结构定义为案例类有什么缺点呢?

它对类或其客户有什么限制?

在某些情况下您应该更喜欢非案例课程吗?

请参阅此相关问题:stackoverflow.com/q/4635765/156410
为什么这不是建设性的?这个网站上的模组太严格了。这有有限数量的可能的事实答案。
同意埃洛夫。这也是一个我也想要答案的问题,提供的答案非常有用,而且看起来并不主观。我看到许多“如何修复我的代码摘录”的问题引发了更多的辩论和意见。

C
Community

首先是好的部分:

默认情况下一切都是不可变的

是的,如果需要,甚至可以被覆盖(使用 var

自动定义的吸气剂

可以在任何类中通过在参数前加上 val

体面的toString()实施

是的,非常有用,但如有必要,可以在任何课程上手动完成

符合 equals()hashCode()

结合简单的模式匹配,这是人们使用案例类的主要原因

使用 unapply() 方法进行匹配的伴随对象

也可以使用提取器在任何课程上手动完成

这个列表还应该包括超级强大的复制方法,这是 Scala 2.8 中最好的东西之一

然后不好的是,案例类只有少数真正的限制:

您不能使用与编译器生成的方法相同的签名在伴随对象中定义应用

但在实践中,这很少成为问题。生成的 apply 方法的更改行为保证会让用户感到惊讶,并且应该强烈劝阻,这样做的唯一理由是验证输入参数 - 最好在主构造函数主体中完成一项任务(这也使得使用 {1 })

你不能子类化

是的,尽管案例类本身仍然可能是后代。一种常见的模式是构建特征的类层次结构,使用案例类作为树的叶节点。

还值得注意的是 sealed 修饰符。具有此修饰符的特征的任何子类必须在同一文件中声明。当对特征实例进行模式匹配时,如果您没有检查所有可能的具体子类,编译器会警告您。当与案例类结合使用时,如果它在没有警告的情况下编译,它可以为您的代码提供非常高水平的信心。

作为 Product 的子类,case 类的参数不能超过 22 个

没有真正的解决方法,除了停止使用这么多参数来滥用类:)

还...

有时注意到的另一个限制是 Scala(当前)不支持惰性参数(如 lazy val,但作为参数)。解决方法是使用别名参数并将其分配给构造函数中的惰性 val。不幸的是,按名称参数不与模式匹配混合使用,这会阻止该技术与案例类一起使用,因为它会破坏编译器生成的提取器。

如果您想实现功能强大的惰性数据结构,这是相关的,并且希望通过在 Scala 的未来版本中添加惰性参数来解决。


感谢您的全面回答。我认为所有例外“你不能继承”可能不太可能很快让我分阶段。
您可以子类化一个案例类。子类也不能是案例类——这是限制。
Scala 2.11 中删除了案例类的 22 个参数限制。 issues.scala-lang.org/browse/SI-7296
断言“您不能使用与编译器生成的方法相同的签名在伴随对象中定义应用”是不正确的。虽然它需要跳过一些环节才能做到这一点(如果您打算保留以前由 scala 编译器不可见地生成的功能),但肯定可以实现:stackoverflow.com/a/25538287/501113
我一直在广泛使用 Scala 案例类,并提出了一个“案例类模式”(最终将成为 Scala 宏),它有助于解决上面确定的一些问题:codereview.stackexchange.com/a/98367/4758
L
LoicTheAztec

一个很大的缺点:一个案例类不能扩展一个案例类。这就是限制。

您错过的其他优势,为了完整性而列出:兼容的序列化/反序列化,无需使用“new”关键字来创建。

对于具有可变状态、私有状态或无状态的对象(例如大多数单例组件),我更喜欢非案例类。几乎所有其他的案例类。


您可以子类化一个案例类。子类也不能是案例类——这是限制。
D
Daniel C. Sobral

我认为 TDD 原则适用于此:不要过度设计。当您声明某事物为 case class 时,您声明了很多功能。这将降低您将来更改课程的灵活性。

例如,case class 在构造函数参数上具有 equals 方法。当你第一次编写你的类时,你可能并不关心这一点,但后来,你可能会决定你希望相等性忽略其中一些参数,或者做一些不同的事情。但是,客户端代码可能会同时编写,这取决于 case class 相等性。


我认为客户端代码不应该取决于“等于”的确切含义;由班级决定“等于”对它意味着什么。课程作者应该可以自由地更改“等于”的实现。
@pkaeding 您可以自由地让客户端代码不依赖于任何私有方法。公开的一切都是您同意的合同。
@DanielC.Sobral 是的,但是 equals() 的确切实现(它基于哪些字段)不一定在合同中。至少,您可以在第一次编写类时明确地将其从合同中排除。
@DanielC.Sobral 您自相矛盾:您说人们甚至会依赖默认的 equals 实现(比较对象身份)。如果这是真的,并且您稍后编写了不同的 equals 实现,他们的代码也会中断。无论如何,如果您指定前置/后置条件和不变量,而人们忽略它们,那就是他们的问题。
@herman我所说的没有矛盾。至于“他们的问题”,当然,除非它成为你的问题。比如说,因为他们是你创业公司的大客户,或者因为他们的经理说服高层管理人员改变他们的成本太高,所以你必须撤销你的改变,或者因为改变会导致数百万美元错误并被还原等。但是如果您是出于爱好编写代码并且不关心用户,请继续。
C
Community

在某些情况下您应该更喜欢非案例课程吗?

Martin Odersky 在他的课程 Functional Programming Principles in Scala(第 4.6 课 - 模式匹配)中为我们提供了一个很好的起点,当我们必须在类和案例类之间进行选择时,我们可以使用它。 Scala By Example的第 7 章包含相同的示例。

比如说,我们想为算术表达式编写一个解释器。为了让事情最初变得简单,我们将自己限制在数字和 + 操作上。这样的表达式可以表示为一个类层次结构,一个抽象基类 Expr 作为根,两个子类 Number 和 Sum。然后,表达式 1 + (3 + 7) 将表示为 new Sum( new Number(1), new Sum( new Number(3), new Number(7)))

abstract class Expr {
  def eval: Int
}

class Number(n: Int) extends Expr {
  def eval: Int = n
}

class Sum(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval + e2.eval
}

此外,添加新的 Prod 类并不需要对现有代码进行任何更改:

class Prod(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval * e2.eval
}

相反,添加新方法需要修改所有现有类。

abstract class Expr { 
  def eval: Int 
  def print
} 

class Number(n: Int) extends Expr { 
  def eval: Int = n 
  def print { Console.print(n) }
}

class Sum(e1: Expr, e2: Expr) extends Expr { 
  def eval: Int = e1.eval + e2.eval
  def print { 
   Console.print("(")
   print(e1)
   Console.print("+")
   print(e2)
   Console.print(")")
  }
}

案例类解决了同样的问题。

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
}
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr

添加新方法是本地更改。

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
  }
}

添加新的 Prod 类可能需要更改所有模式匹配。

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
    case Prod(e1,e2) => e1.eval * e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
    case Prod(e1,e2) => ...
  }
}

视频讲座的文字记录 4.6 Pattern Matching

这两种设计都非常好,有时在它们之间进行选择是一种风格问题,但仍然有一些重要的标准。一个标准可能是,您是更频繁地创建新的表达式子类还是更频繁地创建新方法?因此,它是一个标准,着眼于系统的未来可扩展性和可能的扩展通行证。如果您所做的主要是创建新的子类,那么实际上面向对象的分解解决方案占了上风。原因是使用 eval 方法创建一个新的子类非常简单且非常局部的更改,而在功能解决方案中,您必须返回并更改 eval 方法中的代码并添加一个新案例给它。另一方面,如果你要做的是创建许多新方法,但类层次结构本身会保持相对稳定,那么模式匹配实际上是有利的。因为,模式匹配解决方案中的每个新方法都只是局部更改,无论您将其放在基类中,还是放在类层次结构之外。而一个新的方法,比如面向对象的分解中的 show 需要一个新的增量是每个子类。所以会有更多的部分,你必须触摸。因此,这种在二维中的可扩展性问题,您可能想要将新类添加到层次结构,或者您可能想要添加新方法,或者两者兼而有之,已被命名为表达式问题。

记住:我们必须把它当作一个起点,而不是唯一的标准。

https://i.stack.imgur.com/XWhhv.png


a
arglee

我在 Alvin Alexander 第 6 章:objects 中引用了 Scala cookbook 的内容。

这是我在这本书中发现的许多有趣的事情之一。

要为案例类提供多个构造函数,了解案例类声明的实际作用很重要。

case class Person (var name: String)

如果您查看 Scala 编译器为案例类示例生成的代码,您会看到它创建了两个输出文件,Person$.class 和 Person.class。如果您使用 javap 命令反汇编 Person$.class,您会看到它包含一个 apply 方法以及许多其他方法:

$ javap Person$
Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction1 implements scala.ScalaObject,scala.Serializable{
public static final Person$ MODULE$;
public static {};
public final java.lang.String toString();
public scala.Option unapply(Person);
public Person apply(java.lang.String); // the apply method (returns a Person) public java.lang.Object readResolve();
        public java.lang.Object apply(java.lang.Object);
    }

您还可以反汇编 Person.class 以查看它包含的内容。对于这样一个简单的类,它包含额外的 20 个方法;这种隐藏的膨胀是一些开发人员不喜欢案例类的原因之一。