使用trait实现抽象方法,编译器行为不一致?

6

我有一个基类,它来自于一个Java库,我无法修改它的代码。这个类(A)有一个空方法(b),本应该声明为抽象方法:

class A {
  def b { }
}

我在Scala中扩展了这个类并重写了该方法以使其成为抽象方法:

abstract class AA extends A {
  override def b
}

现在我在一个trait中实现了这种方法:
trait B {
  def b { println("B") }
}

如果我使用trait B扩展AA,会出现错误:覆盖类A中类型为Unit的方法b; 需要在trait B中添加`override'修饰符的类型为Unit的方法b。
class C extends AA with B {}

相反,如果代码像这样,所有内容都将被编译而不会出现错误,这对我来说有点矛盾:

abstract class AA {
  def b
}

trait B {
  def b { println("B") }
}

class C extends AA with B {}

我正在运行Scala 2.8.0RC3,对这门语言完全不熟悉(仅3天)。另一个奇怪且相关的行为是,在将b声明为抽象时,不需要使用override标签:

abstract class AA extends A {
  def b
}

这是否与您想要通过混合在特质中定义的自己的def toString():String实现来替换Java类的Java toString实现相同的问题? - huynhjl
不,这不是同一个情况。在那种情况下,编译器的行为是完全合理的,因为无法确定应该使用哪个定义:Java toString 还是 mixin 的定义。但在我的情况下,只有一个可用。 - A.R
3个回答

6

为了尝试了解发生了什么,我尝试了以下内容:

scala> class A{
     |   def b{ }
     | }
defined class A

scala> abstract class AA extends A{
     |   override def b
     | }
defined class AA

scala> class AAA extends AA{
     |   def b = println("AAA")
     | }
<console>:8: error: overriding method b in class A of type => Unit;
 method b needs `override' modifier
         def b = println("AAA")
             ^

显然,问题的根源在于抽象类无法从其超类中“释放”方法,以使抽象类的子类不必包含“override”修饰符。

好的,override 的问题可以通过在 C 类中使用 override def b = super[B].b 来消除编译器的歧义,但这并不是必要的。这就是我的观点。谢谢! - A.R
1
MJP 是正确的:具体成员在子类中永远无法变为抽象。在子类中声明抽象成员没有任何效果。这是混合组合规则的结果,其中具体始终优先于抽象,无论包含定义的特征的顺序如何。 - Martin Odersky

2

不确定这是否是正确的解决方案,但如果你的trait B继承了A(并覆盖了b),那么一切都会编译正常:

首先让我们像你在问题中展示的那样定义AAA

C:\Users\VonC>scala
Welcome to Scala version 2.8.0.RC5 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_18).
Type in expressions to have them evaluated.
Type :help for more information.

scala> class A {
     | def b { println("A b") }
     | }
defined class A

scala> new A
res5: A = A@153bedc4

scala> res5.b
A b

scala> abstract class AA extends A {
     | override def b
     | }
defined class AA

您所做的事情:

scala> trait B {
     | override def b { println("B b") }
     | }
<console>:6: error: method b overrides nothing
       override def b { println("B b") }
                    ^

我尝试使用特质B(为了能够添加'override'):

scala> trait B extends A {
     | override def b { println("B b") }
     | }
defined trait B

所以现在:
scala> class C extends AA with B {}
defined class C

scala> new C
res7: C = C@1497b7b1

scala> res7.b
B b

正确的b重写方法使用 C.b 被调用。
至于你所表现出的“不一致性”,参见Scala for Java Refugees Part 5: Traits and Types

首先,有那个总是让人烦躁的override关键字。在基本面向对象编程的文章中,我提到了任何覆盖超类中方法的方法都必须使用覆盖修饰符进行声明。当时,我将其比作语言强制使用@Override注释的方式,其主要目的是强制执行良好实践。

特质的真正关键在于编译器在继承类中对它们处理的方式。
特质实际上是混入,而不是真正的父类
任何非抽象特质成员实际上都包含在继承类中,就像物理上一样成为类的一部分。嗯,不是物理上的,但你懂的。
就好像编译器使用非抽象成员进行剪切和粘贴,并将它们插入继承类中一样。这意味着继承路径没有歧义,也没有菱形问题。

所以在你的第二个例子中不需要override关键字。

感谢您详细的回复。我认为我理解了traits,因为我来自Ruby,它有模块,类似的东西。在我看来,让trait B扩展A违背了traits的目的,因为我无法在另一个类层次结构中使用此trait,这正是我想要做的。有什么想法吗?我认为问题在于Scala编译器没有看到方法b已经变成了抽象方法,在将trait混入叶子类C时,其定义没有歧义。 - A.R
@A.R:我明白了;我的回答受到了http://blog.objectmentor.com/articles/2008/09/29/a-scala-style-_with_-construct-for-ruby的启发。还在继续寻找其他解决方案。 - VonC

2
问题非常微妙。一般来说,扩展 A 的类 AA 应该与扩展 A 的 traits 混合使用。
你做的是:
class A {
  def b { }
}

abstract class AA extends A {
  override def b
}

trait B {
  def b { println("B") }
}

因此,当您混合使用AAB方法时,b被定义了两次。一次是由A定义的(因为在B中的定义替换了AA中的覆盖),第二次是由B定义的。编译器无法选择其中之一,因为这两个方法之间没有层次关系(同名但不相关)。如果您愿意,可以这样想:编译器“混合”AAB的内容;如果他选择了AA的方法,它将是抽象的,如果他选择了B的方法(应该发生的情况),因为它不是一个覆盖,您将会卡在两个方法b中。
要解决这个问题,您需要确保两个方法override相同的方法,这样编译器就会理解您正在谈论相同的方法,并优先考虑最后混合的特性。
现在,要在B中覆盖方法b,该类还必须继承自A。因此,最常规的做法是:
class A {
  def b { }
}

abstract class AA extends A {
  override def b
}

trait B extends A{
  def b { println("B") }
}

class C extends AA with B {}

这段代码可以正常编译。

现在,当你执行以下操作时:

abstract class AA {
  def b
}

trait B {
  def b { println("B") }
}

class C extends AA with B {}

很明显这两种方法是一样的,因此编译器知道它必须使用trait中的方法。

其他解决方案包括:

  1. 使B覆盖AA
  2. 使A中的b为抽象方法(但你不想要这个)

再次强调,这个问题非常微妙,但我希望我的解释能让它更加清晰。如果想更好地了解,请阅读Scala的可堆叠特质模式


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