声明 Scala 案例类有哪些缺点?
如果您正在编写使用大量漂亮的、不可变的数据结构的代码,则案例类似乎是天赐之物,只需一个关键字即可免费为您提供以下所有功能:
- 默认情况下一切都是不可变的
- Getters 自动定义
- 体面的 toString() 实现
- 兼容equals() 和 hashCode()
- 伴随对象与 unapply() 方法进行匹配
但是将不可变数据结构定义为案例类有哪些缺点呢?
它对班级或其客户施加了哪些限制?
在某些情况下您应该更喜欢非案例课程吗?
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
But what are the disadvantages of defining an immutable data structure as a case class?
What restrictions does it place on the class or its clients?
Are there situations where you should prefer a non-case class?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
首先是好的方面:
默认情况下一切都是不可变的
是的,如果需要的话甚至可以覆盖(使用
var
)自动获取器定义
可以在任何类中通过在参数前添加
val
体面的
toString()
实现是的,非常有用,但如果需要,可以在任何类上手动完成
兼容
equals()
和hashCode()
与简单的模式匹配相结合,这是人们使用案例类的主要原因
带有
unapply()
方法的 Companion 对象进行匹配也可以使用提取器手动处理任何类
这个列表还应该包括超级强大的复制方法,这是 Scala 2.8 中最好的东西之一
然后不好的是,案例类只有少数真正的限制:
您不能使用与编译器生成的方法相同的签名在伴生对象中定义
apply
但实际上,这很少是问题。更改生成的 apply 方法的行为肯定会让用户感到惊讶,并且强烈建议不要这样做,这样做的唯一理由是验证输入参数 - 最好在主构造函数主体中完成这项任务(这也使得在使用
时可以进行验证>copy
)你不能子类化
True,尽管案例类本身仍然有可能是后代。一种常见的模式是构建特征的类层次结构,使用案例类作为树的叶节点。
还值得注意的是
sealed
修饰符。具有此修饰符的特征的任何子类必须在同一文件中声明。当对特征的实例进行模式匹配时,如果您没有检查所有可能的具体子类,编译器可以警告您。当与案例类结合使用时,如果代码在没有警告的情况下进行编译,则可以为您提供非常高水平的信心。作为 Product 的子类,case 类不能有超过 22 个参数
没有真正的解决方法,除了停止滥用具有这么多参数的类:)
另外...
有时会注意到的另一个限制是 Scala(当前)不支持惰性参数(类似于惰性值,但作为参数)。解决方法是使用按名称参数并将其分配给构造函数中的惰性 val。不幸的是,按名称参数不会与模式匹配混合,这会阻止该技术与案例类一起使用,因为它会破坏编译器生成的提取器。
如果您想要实现功能强大的惰性数据结构,这一点是相关的,并且有望通过在 Scala 的未来版本中添加惰性参数来解决。
First the good bits:
Everything immutable by default
Yes, and can even be overridden (using
var
) if you need itGetters automatically defined
Possible in any class by prefixing params with
val
Decent
toString()
implementationYes, very useful, but doable by hand on any class if necessary
Compliant
equals()
andhashCode()
Combined with easy pattern-matching, this is the main reason that people use case classes
Companion object with
unapply()
method for matchingAlso 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
Then the bad, there are only a handful of real restrictions with case classes:
You can't define
apply
in the companion object using the same signature as the compiler-generated methodIn 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
)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.
It's also worth noting the
sealed
modifier. Any subclass of a trait with this modifier must be 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.As a subclass of Product, case classes can't have more than 22 parameters
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 val
s, 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.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.
一个很大的缺点:案例类不能扩展案例类。这就是限制。
您错过的其他优点,为了完整性而列出:兼容的序列化/反序列化,无需使用“new”关键字来创建。
对于具有可变状态、私有状态或无状态的对象(例如大多数单例组件),我更喜欢非大小写类。几乎所有其他内容的案例课程。
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.
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.
我认为 TDD 原则适用于此:不要过度设计。当您将某个东西声明为
case 类
时,您就声明了很多功能。这会降低你将来改变班级的灵活性。例如,
case 类
在构造函数参数上有一个equals
方法。当你第一次编写你的类时,你可能不关心这一点,但是,后来,你可能会决定你希望平等忽略其中一些参数,或者做一些不同的事情。但是,客户端代码可能会同时编写,这取决于case class
相等性。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.For example, a
case class
has anequals
method 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 oncase class
equality.Martin Odersky 在他的课程Scala 函数式编程原理(讲座 4.6 - 模式)中为我们提供了一个良好的起点当我们必须在类和案例类之间进行选择时可以使用匹配)。
Scala By Examples 的第 7 章包含相同的示例。
此外,添加新的 Prod 类不需要对现有代码进行任何更改:
相反,添加新方法需要修改所有现有类。
案例类解决了同样的问题。
添加新方法是局部更改。
添加新的 Prod 类可能需要更改所有模式匹配。
视频讲座的文字记录4.6 模式匹配
请记住:我们必须将此作为起点,而不是唯一的标准。
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 Example contains the same example.
Furthermore, adding a new Prod class does not entail any changes to existing code:
In contrast, add a new method requires modification of all existing classes.
The same problem solved with case classes.
Adding a new method is a local change.
Adding a new Prod class requires potentially change all pattern matching.
Transcript from the videolecture 4.6 Pattern Matching
Remember: we must use this like a starting point and not like the only criteria.
我引用了
Alvin Alexander
的Scala Cookbook
第 6 章:对象
。这是我在这本书中发现的众多有趣的事情之一。
要为案例类提供多个构造函数,了解案例类声明的实际用途非常重要。
如果您查看 Scala 编译器为 case 类示例生成的代码,您会发现它创建了两个输出文件:Person$.class 和 Person.class。如果使用 javap 命令反汇编 Person$.class,您将看到它包含一个 apply 方法以及许多其他方法:
您还可以反汇编 Person.class 以查看它包含的内容。对于像这样的简单类,它包含额外的 20 个方法;这种隐藏的膨胀是一些开发人员不喜欢案例类的原因之一。
I am quoting this from
Scala cookbook
byAlvin Alexander
chapter 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.
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:
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.