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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/4653424/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-22 02:42:47  来源:igfitidea点击:

What are the disadvantages to declaring Scala case classes?

scalacase-class

提问by Graham Lea

If you're writing code that's using lots of beautiful, immutable data structures, case classes appear to be a godsend, giving you all of the following for free with just one keyword:

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

  • Everything immutable by default
  • Getters automatically defined
  • Decent toString() implementation
  • Compliant equals() and hashCode()
  • Companion object with unapply() method for matching
  • 默认情况下一切都是不可变的
  • 自动定义的吸气剂
  • 体面的 toString() 实现
  • 兼容 equals() 和 hashCode()
  • 带有用于匹配的 unapply() 方法的伴侣对象

But what are the disadvantages of defining an immutable data structure as a case class?

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

What restrictions does it place on the class or its clients?

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

Are there situations where you should prefer a non-case class?

在某些情况下,您应该更喜欢非 case 类吗?

采纳答案by Dave Griffith

One big disadvantage: a case classes can't extend a case class. That's the restriction.

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

Other advantages you missed, listed for completeness: compliant serialization/deserialization, no need to use "new" keyword to create.

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

I prefer non-case classes for objects with mutable state, private state, or no state (e.g. most singleton components). Case classes for pretty much everything else.

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

回答by Kevin Wright

First the good bits:

首先是好的部分:

Everything immutable by default

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

Yes, and can even be overridden (using var) if you need it

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

Getters automatically defined

自动定义的吸气剂

Possible in any class by prefixing params with val

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

Decent toString()implementation

体面的toString()实施

Yes, very useful, but doable by hand on any class if necessary

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

Compliant equals()and hashCode()

合规equals()hashCode()

Combined with easy pattern-matching, this is the main reason that people use case classes

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

Companion object with unapply()method for matching

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

Also possible to do by hand on any class by using extractors

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

This list should also include the uber-powerful copy method, one of the best things to come to Scala 2.8

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



Then the bad, there are only a handful of real restrictions with case classes:

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

You can't define applyin the companion object using the same signature as the compiler-generated method

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

In practice though, this is rarely a problem. Changing behaviour of the generated apply method is guaranteed to surprise users and should be strongly discouraged, the only justification for doing so is to validate input parameters - a task best done in the main constructor body (which also makes the validation available when using copy)

但在实践中,这很少成为问题。更改生成的 apply 方法的行为肯定会让用户感到惊讶,因此强烈建议不要这样做,这样做的唯一理由是验证输入参数 - 最好在主构造函数主体中完成的任务(这也使验证在使用时可用copy

You can't subclass

你不能子类化

True, though it's still possible for a case class to itself be a descendant. One common pattern is to build up a class hierarchy of traits, using case classes as the leaf nodes of the tree.

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

It's also worth noting the sealedmodifier. Any subclass of a trait with this modifier mustbe declared in the same file. When pattern-matching against instances of the trait, the compiler can then warn you if you haven't checked for all possible concrete subclasses. When combined with case classes this can offer you a very high level level of confidence in your code if it compiles without warning.

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

As a subclass of Product, case classes can't have more than 22 parameters

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

No real workaround, except to stop abusing classes with this many params :)

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

Also...

还...

One other restriction sometimes noted is that Scala doesn't (currently) support lazy params (like lazy vals, but as parameters). The workaround to this is to use a by-name param and assign it to a lazy val in the constructor. Unfortunately, by-name params don't mix with pattern matching, which prevents the technique being used with case classes as it breaks the compiler-generated extractor.

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

This is relevant if you want to implement highly-functional lazy data structures, and will hopefully be resolved with the addition of lazy params to a future release of Scala.

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

回答by Daniel C. Sobral

I think the TDD principle apply here: do not over-design. When you declare something to be a case class, you are declaring a lot of functionality. That will decrease the flexibility you have in changing the class in the future.

我认为 TDD 原则在这里适用:不要过度设计。当您将某物声明为 a 时case class,您就是在声明许多功能。这将降低您将来更改课程的灵活性。

For example, a case classhas an equalsmethod over the constructor parameters. You may not care about that when you first write your class, but, latter, may decide you want equality to ignore some of these parameters, or do something a bit different. However, client code may be written in the mean time that depends on case classequality.

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

回答by gabrielgiussi

Are there situations where you should prefer a non-case class?

在某些情况下,您应该更喜欢非 case 类吗?

Martin Odersky gives us a good starting point in his course Functional Programming Principles in Scala(Lecture 4.6 - Pattern Matching) that we could use when we must choose between class and case class. The chapter 7 of Scala By Examplecontains the same example.

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

Say, we want to write an interpreter for arithmetic expressions. To keep things simple initially, we restrict ourselves to just numbers and + operations. Such expres- sions can be represented as a class hierarchy, with an abstract base class Expr as the root, and two subclasses Number and Sum. Then, an expression 1 + (3 + 7) would be represented as

new Sum( new Number(1), new Sum( new Number(3), new Number(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
}

Furthermore, adding a new Prod class does not entail any changes to existing code:

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

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

In contrast, add a new method requires modification of all existing classes.

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

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

The same problem solved with case classes.

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

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

Adding a new method is a local change.

添加新方法是本地更改。

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

Adding a new Prod class requires potentially change all pattern matching.

添加新的 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) => ...
  }
}

Transcript from the videolecture 4.6 Pattern Matching

视频讲座的抄本4.6 模式匹配

Both of these designs are perfectly fine and choosing between them is sometimes a matter of style, but then nevertheless there are some criteria that are important.

One criteria could be, are you more often creating new sub-classes of expression or are you more often creating new methods?So it's a criterion that looks at the future extensibility and the possible extension pass of your system.

If what you do is mostly creating new subclasses, then actually the object oriented decomposition solution has the upper hand. The reason is that it's very easy and a very local change to just create a new subclass with an eval method, where as in the functional solution, you'd have to go back and change the code inside the eval method and add a new case to it.

On the other hand, if what you do will be create lots of new methods, but the class hierarchy itself will be kept relatively stable, then pattern matching is actually advantageous. Because, again, each new method in the pattern matching solution is just a local change, whether you put it in the base class, or maybe even outside the class hierarchy. Whereas a new method such as show in the object oriented decomposition would require a new incrementation is each sub class. So there would be more parts, That you have to touch.

So the problematic of this extensibility in two dimensions, where you might want to add new classes to a hierarchy, or you might want to add new methods, or maybe both, has been named the expression problem.

这两种设计都非常好,在它们之间进行选择有时是一种风格问题,但仍然有一些重要的标准。

一个标准可能是,您是更频繁地创建新的表达式子类还是更频繁地创建新方法?所以它是一个着眼于系统未来可扩展性和可能的​​扩展通道的标准。

如果你所做的主要是创建新的子类,那么面向对象的分解解决方案实际上占了上风。原因是创建一个带有 eval 方法的新子类非常简单且非常本地化,而在函数式解决方案中,您必须返回并更改 eval 方法中的代码并添加一个新案例到它。

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

因此,这种二维可扩展性的问题,您可能想要向层次结构添加新类,或者您可能想要添加新方法,或者可能两者都被命名为表达式问题

Remember: we must use this like a starting point and not like the only criteria.

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

enter image description here

enter image description here

回答by arglee

I am quoting this from Scala cookbookby Alvin Alexanderchapter 6: objects.

我从这个引用Scala cookbookAlvin Alexander第6章:objects

This is one of the many things that I found interesting in this book.

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

To provide multiple constructors for a case class, it's important to know what the case class declaration actually does.

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

case class Person (var name: String)

If you look at the code the Scala compiler generates for the case class example, you'll see that see it creates two output files, Person$.class and Person.class. If you disassemble Person$.class with the javap command, you'll see that it contains an apply method, along with many others:

如果您查看 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);
    }

You can also disassemble Person.class to see what it contains. For a simple class like this, it contains an additional 20 methods; this hidden bloat is one reason some developers don't like case classes.

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