声明Scala案例类有哪些缺点?

Gra*_*Lea 103 scala case-class

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

  • 默认情况下一切都是不可变的
  • Getters自动定义
  • Decent toString()实现
  • 兼容的equals()和hashCode()
  • Companion对象使用unapply()方法进行匹配

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

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

您是否应该选择非案例类?

Kev*_*ght 98

首先是好位:

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

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

Getters自动定义

可以在任何类中通过为params添加前缀 val

体面的toString()实施

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

符合equals()hashCode()

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

具有unapply()匹配方法的伴随对象

也可以通过使用提取器在任何类上手动完成

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


那么糟糕的是,案例类只有少数真正的限制:

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

但在实践中,这很少是一个问题.改变生成的apply方法的行为可以保证让用户感到惊讶,并且强烈建议不要这样做,这样做的唯一理由就是验证输入参数 - 在主构造函数体中做得最好的任务(在使用时也可以使验证可用copy)

你不能子类

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

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

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

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

也...

有时提到的另一个限制是Scala(目前)不支持懒惰的params(如lazy vals,但作为参数).解决方法是使用副名称param并将其分配给构造函数中的lazy val.不幸的是,by-name params不会与模式匹配混合,这会阻止该技术与case类一起使用,因为它会破坏编译器生成的提取器.

如果您想要实现高功能的惰性数据结构,这是有用的,并且希望通过在未来的Scala版本中添加惰性参数来解决这个问题.

  • 您可以子类化案例类.子类也不能是一个案例类 - 这就是限制. (14认同)
  • 在Scala 2.11中删除了案例类的22参数限制.https://issues.scala-lang.org/browse/SI-7296 (5认同)

Dav*_*ith 49

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

您错过了其他优点,列出了完整性:兼容序列化/反序列化,无需使用"new"关键字来创建.

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

  • 您可以子类化案例类.子类也不能是一个案例类 - 这就是限制. (48认同)

Dan*_*ral 10

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

例如,a case class有一个equals构造函数参数的方法.当你第一次写你的课时你可能不在乎,但是,后者可能决定你想要平等忽略其中一些参数,或做一些不同的事情.但是,客户端代码可能在平均时间内写入,这取决于case class相等性.

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

gab*_*ssi 7

您是否应该选择非案例类?

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

比如,我们想为算术表达式编写一个解释器.为了简单起见,我们仅限于数字和+操作.这样的表达式可以表示为类层次结构,抽象基类Expr作为根,以及两个子类Number和Sum.然后,表达式1 +(3 + 7)将表示为

新的总和(新的数字(1),新的总和(新的数字(3),新的数字(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
}
Run Code Online (Sandbox Code Playgroud)

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

class Prod(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval * e2.eval
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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
Run Code Online (Sandbox Code Playgroud)

添加新方法是本地更改.

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

添加新的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) => ...
  }
}
Run Code Online (Sandbox Code Playgroud)

视频选择4.6模式匹配的成绩单

这两种设计都非常精细,在它们之间进行选择有时是风格问题,但是有一些标准很重要.

一个标准可能是,您是否经常创建新的表达子类,或者您更经常创建新方法?因此,它是一个标准,用于查看未来的可扩展性以及系统可能的扩展过程.

如果你所做的主要是创建新的子类,那么实际上面向对象的分解解决方案占了上风.原因是,使用eval方法创建一个新的子类非常容易并且非常局部更改,在功能解决方案中,您必须返回并更改eval方法中的代码并添加新案例它.

另一方面,如果你所做的将创建许多新方法,但类层次结构本身将保持相对稳定,那么模式匹配实际上是有利的.因为,模式匹配解决方案中的每个新方法都只是一个本地更改,无论是将其放在基类中,还是放在类层次结构之外.然而,诸如在面向对象分解中显示的新方法将需要新的增量是每个子类.所以会有更多的部分,你必须触摸.

因此,在二维中可扩展性存在问题,您可能希望在层次结构中添加新类,或者您可能希望添加新方法,或者两者都被称为表达式问题.

请记住:我们必须使用它作为一个起点而不是唯一的标准.

在此输入图像描述