为什么没有参数反差方法可以覆盖?

时间:2010-06-08 08:55:35

标签: java c++ oop override variance

当重写方法时,C ++和Java支持返回类型协方差。

但是,它们都不支持参数类型的反差 - 相反,它转换为over loading (Java)或隐藏(C ++)。

为什么??在我看来,允许这样做是没有害处的。我可以在Java中找到它的一个原因 - 因为它无论如何都有“选择最特定版本”的重载机制 - 但是不能想到C ++的任何原因。

示例(Java):

class A {
    public void f(String s) {...}
}
class B extends A {
    public void f(Object o) {...} // Why doesn't this override A.f?
}

6 个答案:

答案 0 :(得分:22)

关于反方差的纯问题

为一种语言添加逆变量会打开很多潜在的问题或不清洁的解决方案,并且提供很少的优势,因为它可以在没有语言支持的情况下轻松模拟:

struct A {};
struct B : A {};
struct C {
   virtual void f( B& );
};
struct D : C {
   virtual void f( A& );     // this would be contravariance, but not supported
   virtual void f( B& b ) {  // [0] manually dispatch and simulate contravariance
      D::f( static_cast<A&>(b) );
   }
};

通过简单的额外跳转,您可以手动克服不支持反差的语言问题。在示例中,f( A& )不需要是虚拟的,并且调用完全限定以禁止虚拟调度机制。

这种方法显示了在对没有完全动态调度的语言添加逆方差时出现的首要问题之一:

// assuming that contravariance was supported:
struct P {
   virtual f( B& ); 
};
struct Q : P {
   virtual f( A& );
};
struct R : Q {
   virtual f( ??? & );
};

在有效的情况下,Q::f会覆盖P::f,对于可以作为o的参数的每个对象P::f都可以,同一个对象 Q::f的有效参数。现在,通过向层次结构添加额外级别,我们最终会遇到设计问题:R::f(B&)P::f的有效覆盖还是R::f(A&)

没有逆变R::f( B& )显然是P::f的覆盖,因为签名是完美的匹配。一旦你向中间级别添加了逆向性,问题就是有些参数在Q级别有效,但不在PR级别。要使R满足Q要求,唯一的选择是强制签名为R::f( A& ),以便以下代码可以编译:

int main() {
   A a; R r;
   Q & q = r;
   q.f(a);
}

与此同时,语言中没有任何内容可以禁止以下代码:

struct R : Q {
   void f( B& );    // override of Q::f, which is an override of P::f
   virtual f( A& ); // I can add this
};

现在我们有一个有趣的效果:

int main() {
  R r;
  P & p = r;
  B b;
  r.f( b ); // [1] calls R::f( B& )
  p.f( b ); // [2] calls R::f( A& )
}

在[1]中,可以直接调用R的成员方法。由于r是本地对象而不是引用或指针,因此没有动态分派机制,最佳匹配为R::f( B& )。同时,在[2]中,通过对基类的引用进行调用,并启动虚拟调度机制。

由于R::f( A& )Q::f( A& )的覆盖,后者又是P::f( B& )的覆盖,因此编译器应调用R::f( A& )。虽然这可以在语言中完美定义,但发现两个几乎完全调用[1]和[2]实际上调用不同的方法可能会令人惊讶,并且在[2]中系统会调用而不是参数的最佳匹配。

当然,可以区别对待:R::f( B& )应该是正确的覆盖,而不是R::f( A& )。这种情况下的问题是:

int main() {
   A a; R r;
   Q & q = r;
   q.f( a );  // should this compile? what should it do?
}

如果您检查Q类,则前面的代码完全正确:Q::fA&为参数。编译器没有理由抱怨该代码。但问题是,在最后一个假设下,R::f需要B&而不是A&作为参数!即使在调用地点的方法签名看起来完全正确,实际的覆盖也无法处理a参数。这条路径使我们确定第二条路径比第一条路径差得多。 R::f( B& )不能覆盖Q::f( A& )

遵循最少意外的原则,对于编译器实现者和程序员来说,在函数参数中不要有矛盾方差要简单得多。不是因为它不可行,而是因为代码中存在怪癖和惊喜,并且如果该语言中没有该功能,则考虑到简单的解决方法。

重载与隐藏

在Java和C ++中,在第一个示例中(ABCD)删除手动调度[0],{{1 }和C::f是不同的签名而不是覆盖。在这两种情况下,它们实际上都是相同函数名的重载,但由于C ++查找规则的原因,D::f重载将由C::f隐藏。但这只意味着编译器默认情况下不会找到隐藏重载,而不是它不存在:

D::f

在类定义稍有变化的情况下,可以使其与Java中的完全相同:

int main() {
   D d; B b;
   d.f( b );    // D::f( A& )
   d.C::f( b ); // C::f( B& )
}

答案 1 :(得分:14)

class A {
    public void f(String s) {...}
    public void f(Integer i) {...}
}

class B extends A {
    public void f(Object o) {...} // Which A.f should this override?
}

答案 2 :(得分:5)

对于C ++,Stroustrup在The Design & Evolution of C++的第3.5.3节中简要讨论了隐藏的原因。他的推理是(我解释)其他解决方案引发了同样多的问题,而且自从C With Classes天以来就是这样。

作为一个例子,他给出了两个类 - 和一个派生类B.它们都有一个虚拟的copy()函数,它接受各自类型的指针。如果我们说:

A a;
B b;
b.copy( & a );

这是一个错误,因为B的副本()隐藏了A.如果它不是错误,只有B的A部分可以通过A的copy()函数更新。

再一次,我已经解释了 - 如果你有兴趣,请阅读这本书,这本书很棒。

答案 3 :(得分:3)

虽然这在任何oo语言中都是不错的,但我仍然需要在我目前的工作中遇到它的适用性。

也许真的不需要它。

答案 4 :(得分:2)

感谢Donroby上面给出的回答 - 我只是在延伸它。

interface Alpha
interface Beta
interface Gamma extends Alpha, Beta
class A {
    public void f(Alpha a)
    public void f(Beta b)
}
class B extends A {
    public void f(Object o) {
        super.f(o); // What happens when o implements Gamma?
    }
}

您遇到的问题类似于不鼓励多个实现继承的原因。 (如果您尝试直接调用A.f(g),则会出现编译错误。)

答案 5 :(得分:1)

感谢donroby和David的回答,我想我明白引入参数反差的主要问题是与重载机制的集成

因此,不仅多个方法的单个覆盖存在问题,而且另一种方式:

class A {
    public void f(String s) {...}
}

class B extends A {
    public void f(String s) {...} // this can override A.f
    public void f(Object o) {...} // with contra-variance, so can this!
}

现在,同一方法有两个有效的覆盖:

A a = new B();
a.f(); // which f is called?

除了超载问题,我想不出别的什么。

编辑:我发现this C++ FQA entry (20.8)与上述内容一致 - 存在重载会导致参数反差的严重问题。