混合和继承有什么区别?

21

我试图在Scala的上下文中理解Mixins。特别是我想知道继承和Mixins之间的概念差异。

维基百科中对Mixin的定义如下:

一个mixin类充当父类,包含所需的功能。子类可以继承或仅重用此功能,但不能作为专门化的手段。通常,mixin将所需的功能导出到子类,而不创建严格的单一“是一个”关系。这里有一个重要的区别,即mixins和继承概念之间的区别,在于子类仍然可以继承父类的所有功能,但关于子类“是一个”父类的语义不一定适用

在上述定义中,我不明白加粗的语句。那是什么意思呢?

  1. 子类可以通过mixin继承功能,但不能作为专门化的手段。
  2. 在mixins中,子类继承了父类的所有功能,但关于子类“是一种”父类的语义不一定适用。- 子类如何扩展父类而不一定是父类的一种类型?是否有这样的例子。

在Scala中,将Mixins视为一种整洁的编译时转换,它将使用额外方法装饰某些类型。在这种情况下,虽然Scala跟踪“混合类型”以供类型检查器使用,但方法定义本身被展平到实际类型中,因此在JVM类中没有建立父子关系。继承通常与运行时多态方法解析相关联,但Mixins(在很大程度上)是正交概念。Java 8接口默认方法也是Mixins。 - user2864740
这个问题似乎在更广泛的背景下已经被问过了(尽管我对那里的答案并不完全满意)。 - badcook
1
书籍[Scala编程第三版][1]的第12章Traits中有一个非常详细的答案描述。 - Adelin
5个回答

20

我不确定我是否正确理解了你的问题,但如果我的理解是正确的,你正在问如何在继承中实现继承而不真正意味着继承。

然而,混合(Mixins)并不是继承——它实际上更类似于将一组方法动态添加到一个对象中。而继承表示“这个东西是另一个东西的一种”,混合则表示“这个对象具有另一个东西的某些特征”。你可以从用于声明混合的关键字中看到这一点:trait

为了大胆地借用 Scala 主页上的例子:

abstract class Spacecraft {
  def engage(): Unit
}
trait CommandoBridge extends Spacecraft {
  def engage(): Unit = {
    for (_ <- 1 to 3)
      speedUp()
  }
  def speedUp(): Unit
}
trait PulseEngine extends Spacecraft {
  val maxPulse: Int
  var currentPulse: Int = 0
  def speedUp(): Unit = {
    if (currentPulse < maxPulse)
      currentPulse += 1
  }
}
class StarCruiser extends Spacecraft
                     with CommandoBridge
                     with PulseEngine {
  val maxPulse = 200
}
在这种情况下,StarCruiser并不是一个CommandoBridge或者PulseEngine;然而它拥有它们,并且获得了在这些trait中定义的方法。正如你可以看到因为它继承自那个类,它是一个 Spacecraft
值得一提的是,当一个trait扩展了一个class时,如果你想用那个trait去创建一些东西,它必须扩展那个类。例如,如果我有一个class Dog,我不能拥有一个Dog with PulseEngine,除非Dog扩展了Spacecraft。以这种方式来说,它并不完全像添加方法;但是,它仍然是相似的。

2
在这种情况下,StarCruiser不是CommandoBridge或PulseEngine。但是,当我定义一个方法,例如launch(foo:CommandoBridge),并调用它时,我真的不明白为什么会这样...将StarCruiser的实例传递给期望CommandoBridge的方法的能力难道不是展示StartCruiser确实是CommandoBridge的典型示例吗? - dade
@dade 这更多是一种笨拙的语法而不是其他什么(否则你怎么能将 CommandoBridge "从" StarCruiser 中取出呢?)当然还有其他方法,但它们都不是特别美观,所以 Scala 设计者选择了他们认为最好的方式。如果我要猜测他们的理由,我会说这是因为他们不想在可以重载现有事物时添加额外的语法。 - anon
1
@dade 我真的不想听起来像是在抨击Scala,但似乎Scala的设计师试图纠正其他语言中一些“有问题”的概念,但最终却引入了更加复杂和模糊的概念。这让开发者无法达成一致,从而造成更多的混乱! - Adelin

5

特质(与类混合时称为mixin)类似于Java中的接口(尽管存在许多差异),您可以向类添加其他功能而无需具有“is a”关系。或者您可以说,通常,特质捆绑了可以由多个独立类使用的功能。

为了给您提供Scala库的示例,Ordered[A]是一个trait,它为可以具有自然排序数据的类提供了一些基本比较操作的实现(如<<=>>= )。

例如,假设您拥有自己的类Number和子类EvenNumberOddNumber,如下所示。

class Number(val num : Int) extends Ordered[Number] {
  override def compare(that : Number) = this.num - that.num
}

trait Half extends Number {
  def half() = num / 2
}

trait Increment extends Number {
  def increment() = num + 1
}

class EvenNumber(val evenNum : Int) extends Number(evenNum) with Half

class OddNumber(val oddNum : Int) extends Number(oddNum) with Increment

在上面的例子中,类 EvenNumberOddNumberNumber 共享“is a”关系,但 EvenNumber 既不与 Half 具有“is a”关系,也不与 OddNumber 具有“is a”关系。 另一个重要的点是,尽管类 Number 使用了 extends Ordered 语法,它意味着 NumberOrdered 的超类 Any 存在隐含的“is a”关系。

但是EvenNumber与Half没有“is a”的关系,也没有OddNumber与Increment共享“is a”的关系。 val h: Half = new EvenNumber(10) val i: Increment = new OddNumber(1)这段代码可以编译和执行,这证明了之前的说法是不正确的。 - Adelin

4
我认为这很取决于使用情况。Scala作为一种多范式语言,使其强大,但有时也会让人感到困惑。
我认为混入(Mixins)在正确使用时非常强大。
混入应该用于引入行为并减少样板代码。
在Scala中,特质(Trait)可以有实现,而且很容易扩展和使用它们。
特质可用于继承。它也可以称为混入,但在我看来,这不是使用混入行为的最佳方式。在这种情况下,您可以将特质视为Java抽象类。其中,您获得的子类是超类(特质)的“类型”。
但是,特质也可以用作适当的混入。现在使用特质作为混入取决于实现方式,即“如何混合”。通常,这是一个简单的问题,需要问自己。它是“特质的子类是否真正是特质的一种类型,还是特质中的行为是减少样板代码的行为”。通常最好通过将特质混合到对象中而不是扩展特质来创建新类来实现它。
例如,请考虑以下示例:
    //All future versions of DAO will extend this
trait AbstractDAO{
  def getRecords:String
  def updateRecords(records:String):Unit
}
//One concrete version
trait concreteDAO extends AbstractDAO{
  override def getRecords={"Here are records"}
  override def updateRecords(records:String){
    println("Updated "+records)
  }
}
//One concrete version
trait concreteDAO1 extends AbstractDAO{
  override def getRecords={"Records returned from DAO2"}
  override def updateRecords(records:String){
    println("Updated via DAO2"+records)
  }
}
//This trait just defines dependencies (in this case an instance of AbstractDAO) and defines operations based over that
trait service{
  this:AbstractDAO =>

  def updateRecordsViaDAO(record:String)={  
  updateRecords(record) 
  }
  def getRecordsViaDAO={
  getRecords
  }
}


object DI extends App{
  val wiredObject = new service with concreteDAO //injecting concrete DAO to the service and calling methods
  wiredObject.updateRecords("RECORD1")
  println(wiredObject.getRecords)

  val wiredObject1 = new service with concreteDAO1
  wiredObject1.updateRecords("RECORD2")
  println(wiredObject1.getRecords)

}

concreteDAO是一个特质(trait),它扩展了AbstractDAO,这就是继承。

val wiredObject = new service with concreteDAO - 这是适当的混入行为。由于service特质要求混入AbstractDAO,因此使用Service扩展ConcreteDAO是错误的。相反,您可以创建不同混入的service实例。


Scala 作为一种多范式语言,使其既强大又有时令人感到有些困惑。我认为这使得它变得复杂和不太理想使用。 - Adelin
2
是的,我明白为什么有些人会这样想。这只是个人观点而已。 - Som Bhattacharyya

2
Mixin与继承的区别在于语义层面,而在语法层面上,它们是相同的。混入一个trait或从trait继承,都使用extendswith,它们的语法是一样的。然而,在语义层面上,一个旨在被混入的trait通常与混入它的类没有“is a”关系,这与一个旨在被继承的trait是不同的。对我来说,一个trait是mixin还是parent是很主观的,这经常造成困惑。

1
我认为它谈论的是实际的类层次结构。例如,如果一个Dog扩展自该类(继承),那么它就是Animal的一种类型。它可以在任何适用于Animal参数的地方使用。

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