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

时间:2011-01-11 01:46:29

标签: scala case-class

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

  • 默认情况下一切都是不可变的
  • 自动定义吸气剂
  • Decent toString()实现
  • 符合equals()和hashCode()
  • 使用unapply()方法进行匹配的伴随对象

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

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

是否存在您应该选择非案例类的情况?

5 个答案:

答案 0 :(得分:97)

首先是好位:

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

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

自动定义Getters

可以在任何课程中使用val

加上参数

体面toString()实施

是的,非常有用,但必要时可以在任何课程上进行操作

合规equals()hashCode()

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

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

也可以通过使用提取器

在任何类上手动完成

此列表还应包括超级强大的复制方法,这是Scala 2.8的最佳选择之一


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

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

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

您不能继承

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

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

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

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

另外...

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

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

答案 1 :(得分:49)

一个很大的缺点:案例类无法扩展案例类。这是限制。

您遗漏的其他优点,列出了完整性:合规序列化/反序列化,无需使用“new”关键字来创建。

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

答案 2 :(得分:10)

我认为TDD原则适用于此:不要过度设计。当您声明某些内容为case class时,您宣布了许多功能。这将降低您将来改变课程的灵活性。

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

答案 3 :(得分:6)

  

是否存在您应该选择非案例类的情况?

Martin Odersky在我的课程Functional Programming Principles in Scala(第4.6讲 - 模式匹配)中为我们提供了一个很好的起点,当我们必须在课堂和案例类之间进行选择时,我们可以使用它。 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
}

此外,添加新的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方法中的代码并为其添加一个新案例。

     

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

     

因此,您可能希望在层次结构中添加新类,或者您可能想要添加新方法,或者两者兼而有之,这两个维度的可扩展性问题已被命名为表达式问题

请记住:我们必须将此作为一个起点,而不是唯一的标准。

enter image description here

答案 4 :(得分:0)

我引用Scala cookbook Alvin Alexander第6章objects引用此内容。

这是我在本书中发现的众多内容之一。

要为案例类提供多个构造函数,重要的是要知道案例类声明实际上做了什么。

case class Person (var name: String)

如果你查看Scala编译器为case类示例生成的代码,你会看到它创建了两个输出文件,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个方法;这个隐藏的膨胀是一些开发人员不喜欢案例类的原因之一。