Scala 案例类继承

100
我有一个基于Squeryl的应用程序。我将我的模型定义为case class,主要是因为我发现拥有copy方法很方便。
我有两个严密相关的模型。字段相同,许多操作都是共用的,并且它们将存储在同一个DB表中。但是,在这两种情况下只有一种情况才有意义或者在两种情况下都有意义但不同的行为。
到目前为止,我只使用了一个单独的case class,并带有区分模型类型的标志。所有根据模型类型不同而不同的方法都以if开头,这非常麻烦,也不太类型安全。
我想做的是将公共行为和字段因子化为祖先case class,并使两个实际模型从中继承。但是,据我所知,Scala中继承自case class并不受欢迎,如果子类本身是case class,则甚至被禁止(我的情况不是)。
继承case class会遇到哪些问题和陷阱?在我的情况下这样做有意义吗?

1
你不能继承自一个非 case 类,或者扩展一个常见的 trait 吗? - Eduardo
我不确定。这些字段是在祖先类中定义的。我想基于这些字段获取复制方法、相等性等等。如果我将父类声明为抽象类,将子类声明为case类,那么它会考虑到在父类上定义的参数吗? - Andrea
我认为你必须在抽象父类(或特质)和目标 case class 中都定义 props。最终会有很多样板代码,但至少是类型安全的。 - virtualeyes
4个回答

132

我避免使用case class继承且不重复代码的首选方式有些显而易见:创建一个公共(抽象)基类:

abstract class Person {
  def name: String
  def age: Int
  // address and other properties
  // methods (ideally only accessors since it is a case class)
}

case class Employer(val name: String, val age: Int, val taxno: Int)
    extends Person

case class Employee(val name: String, val age: Int, val salary: Int)
    extends Person


如果您想更细化,请将属性分组为单独的特征:

trait Identifiable { def name: String }
trait Locatable { def address: String }
// trait Ages { def age: Int }

case class Employer(val name: String, val address: String, val taxno: Int)
    extends Identifiable
    with    Locatable

case class Employee(val name: String, val address: String, val salary: Int)
    extends Identifiable
    with    Locatable

95
你所说的“无代码重复”的地方在哪里?是的,一个合同被定义在case class和其父类之间,但你仍然需要输入props X2。 - virtualeyes
6
@virtualeyes,是的,您仍然需要重复属性,但不必重复方法,因为方法通常比属性包含更多的代码。 - Malte Schwerhoff
1
是的,我只是希望能够解决属性重复的问题——另一个答案提到了类型类作为可能的解决方法;然而,我不确定如何使用它,似乎更适用于混合行为,像特质一样,但更灵活。关于案例类的模板代码,可以接受,如果有其他方式将会非常惊人,真的可以精简大量的属性定义。 - virtualeyes
1
@virtualeyes 我完全同意,如果能够轻松避免属性重复就太好了。编译器插件肯定可以解决这个问题,但我不认为那是一种简单的方法。 - Malte Schwerhoff
16
@virtualeyes,我认为避免代码重复不仅仅是写得更少。对我来说,更重要的是在应用程序的不同部分中没有相同的代码段而它们之间也没有任何联系。通过这种解决方案,所有子类都与一个契约绑定,因此如果父类发生变化,IDE将能够帮助您识别需要修复的代码部分。 - Daniel
显示剩余2条评论

47

因为这是许多人感兴趣的一个有趣主题,所以让我来解释一下。

你可以采用以下方法:

// You can mark it as 'sealed'. Explained later.
sealed trait Person {
  def name: String
}

case class Employee(
  override val name: String,
  salary: Int
) extends Person

case class Tourist(
  override val name: String,
  bored: Boolean
) extends Person

是的,你必须复制这些字段。如果你不这样做,实现正确的相等性(以及其他问题)就会变得不可能。

然而,你不需要复制方法/函数。

如果复制一些属性对你来说很重要,那么可以使用常规类,但请记住它们不适合FP。

或者,你可以使用组合代替继承:

case class Employee(
  person: Person,
  salary: Int
)

// In code:
val employee = ...
println(employee.person.name)

组合是一种有效且可靠的策略,您也应该考虑使用它。

如果您想知道密封特质是什么意思-它是只能在同一个文件中扩展的东西。也就是说,上面的两个case类必须在同一个文件中。这可以实现全面的编译器检查:

val x = Employee(name = "Jack", salary = 50000)

x match {
  case Employee(name) => println(s"I'm $name!")
}

出现错误:

warning: match is not exhaustive!
missing combination            Tourist

这非常有用。现在你不会忘记处理其他类型的Person(人)了。这基本上就是Scala中Option类的作用。

如果这对你没有影响,那么你可以将其设置为非密封类,并将案例类放入它们自己的文件中。并且也许采用组成方式。


1
我认为trait中的def name应该改为val name。我的编译器在前者中给出了无法访问的代码警告。 - BAR

15

case类非常适合作为值对象,即不更改任何属性且可以使用equals进行比较的对象。

但在存在继承的情况下实现equals会变得相当复杂。考虑两个类:

class Point(x : Int, y : Int)
并且
class ColoredPoint( x : Int, y : Int, c : Color) extends Point

根据定义,ColorPoint(1,4,red)应该等于Point(1,4),毕竟它们都是同一个点。那么ColorPoint(1,4,blue)也应该等于Point(1,4),对吗?但当然ColorPoint(1,4,red)不应该等于ColorPoint(1,4,blue),因为它们有不同的颜色。这就是破坏了相等关系的一个基本属性。

更新

你可以使用从特质继承解决许多问题,如另一个答案所述。更灵活的选择通常是使用类型类。请参见What are type classes in Scala useful for?http://www.youtube.com/watch?v=sVMES4RZF-8


我理解并同意这一点。那么,当您有一个处理雇主和员工的应用程序时,您建议应该采取什么措施呢?假设它们共享所有字段(名称、地址等),唯一的区别在于某些方法 - 例如,一个可能想要定义Employer.fire(e: Emplooyee)但另一个不是这样。我想制作两个不同的类,因为它们实际上代表不同的对象,但我也不喜欢产生的重复。 - Andrea
1
这里有一个关于类型类方法的例子吗?比如说针对案例类。 - virtualeyes
@virtualeyes 可以为不同类型的实体提供完全独立的类型,并提供类型类来提供行为。这些类型类可以尽可能地使用继承,因为它们不受案例类的语义契约的约束。这对这个问题有用吗?不知道,这个问题不够具体。 - Jens Schauder
@JensSchauder 看起来特质在行为方面提供了与类型类相同的功能,只是不如类型类灵活。我想表达的是,特质或抽象类通常可以帮助避免案例类属性的重复。 - virtualeyes

9
在这些情况下,我倾向于使用组合而不是继承,即:
sealed trait IVehicle // tagging trait

case class Vehicle(color: String) extends IVehicle

case class Car(vehicle: Vehicle, doors: Int) extends IVehicle

val vehicle: IVehicle = ...

vehicle match {
  case Car(Vehicle(color), doors) => println(s"$color car with $doors doors")
  case Vehicle(color) => println(s"$color vehicle")
}

显然,您可以使用更复杂的层次结构和匹配方式,但希望这能给您一个想法。关键是要利用案例类提供的嵌套提取器。

4
这似乎是此处唯一一个真正没有重复字段的答案。 - Alan Thomas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接