声明Scala case类有什么缺点?
如果你编写的代码使用了很多美丽的,不可变的数据结构,case类似乎是天赐之物,只需要一个关键字就可以免费获得以下所有内容:
- 一切默认是不可变的
- 吸气剂自动定义
- 体面的toString()实现
- 符合equals()和hashCode()
- Companion对象与用于匹配的unapply()方法
但是,将一个不可变的数据结构定义为一个case类有什么缺点?
它对class级或其客户有什么限制?
有没有情况下,你应该更喜欢非案例课?
一个很大的缺点:case类不能扩展case类。 这是限制。
其他优点,你错过了,完整性列出:顺从序列化/反序列化,不需要使用“新”关键字来创build。
我更喜欢非易失性类的对象具有可变状态,私有状态或无状态(例如大多数单例组件)。 案例类几乎所有的一切。
首先是好点子:
一切默认是不可变的
是的,甚至可以重写(使用var
),如果你需要它
吸气剂自动定义
可以在任何类中使用val
前缀params
体面的toString()
实现
是的,非常有用,但如果有必要,可以在任何课堂上手工操作
符合equals()
和hashCode()
结合简单的模式匹配,这是人们使用案例类的主要原因
Companion对象与用于匹配的unapply()
方法
也可以通过使用提取器手动对任何类进行操作
这个列表还应该包括超级强大的复制方法,这是Scala 2.8最好的select之一
那么糟糕的是,在案例类中只有less数真正的限制:
您不能使用与编译器生成的方法相同的签名来在伴随对象中定义apply
但实际上,这很less是一个问题。 改变生成的apply方法的行为可以保证让用户感到惊讶,并且应该强烈build议不要这样做,唯一的理由是validationinput参数 – 一个在主构造器主体中最好的任务(当使用copy
时也是可用的)
你不能inheritance
诚然,尽pipe一个案例类本身仍然是一个后裔。 一个常见的模式是build立一个类特征层次结构,使用案例类作为树的叶节点。
这也是值得注意的sealed
修饰符。 任何具有此修饰符的特征的子类都必须在同一个文件中声明。 当对特征实例进行模式匹配时,如果你没有检查所有可能的具体子类,编译器会发出警告。 当与案例类相结合时,如果编译时没有警告,它可以为您提供非常高级别的信任。
作为产品的子类,案例类不能超过22个参数
没有真正的解决办法,除了停止滥用这个很多参数的类:)
也…
有时候还会注意到另一个限制,那就是Scala没有(当前)支持懒惰的参数(比如lazy val
,而是参数)。 解决这个问题的方法是使用一个名称参数并将其分配给构造函数中的一个惰性val。 不幸的是,名称参数不能与模式匹配混合使用,这会阻止与case类一起使用的技术,因为它会中断编译器生成的提取器。
这是相关的,如果你想实现高function的懒惰数据结构,并希望通过添加懒惰的参数到未来的Scala版本来解决。
我认为TDD原则适用于此:不要过度devise。 当你声明某事是一个case class
,你声明了很多的function。 这会降低你将来改变class级的灵活性。
例如,一个case class
在构造函数参数上有一个equals
方法。 当你第一次写你的课时,你可能并不在意,但后者可能会决定你想要平等地忽略这些参数,或者做一些不同的事情。 然而,客户端代码可能是写在同时取决于case class
平等。
有没有情况下,你应该更喜欢非案例课?
马丁·奥德斯基(Martin Odersky) 在斯卡拉函数编程原理 (第4.6讲模式匹配)中给了我们一个很好的起点,当我们必须在类和案例类之间进行select时,我们可以使用它。 Scala示例的第7章包含相同的示例。
说,我们要写一个算术expression式的解释器。 为了简单起见,我们只限于数字和+操作。 这些expression式可以表示为一个类层次结构,以抽象基类Expr作为根,以及两个子类Number和Sum。 那么,expression式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模式匹配
这两种devise都非常好,在它们之间进行select有时是风格问题,但是有一些重要的标准。
一个标准可能是, 你是否更经常创build新的expression子类,或者你更经常创build新的方法? 所以这是一个关于未来可扩展性和系统可能扩展的标准。
如果你所做的主要是创build新的子类,那么实际上面向对象的分解解决scheme占了上风。 原因在于用eval方法创build一个新的子类是非常容易和非常本地的变化 ,在function性解决scheme中,你必须返回并更改eval方法中的代码并添加一个新的case到它。
另一方面, 如果你所做的将会创造出许多新的方法,但是类层次本身将保持相对稳定,那么模式匹配实际上是有利的。 因为模式匹配解决scheme中的每个新方法都是本地更改 ,无论是将其放在基类中,还是放在类层次结构之外。 而面向对象分解的新方法如需要新的增量是每个子类。 所以会有更多的部分,你必须触摸。
所以这个可扩展性在两个维度的问题,你可能想要添加新的类到一个层次结构,或者你可能想要添加新的方法,或者两者都被命名为expression式的问题 。
请记住:我们必须使用这个起点,而不是像唯一的标准。