54

我遇到了许多反对在 C# 中包含多重继承的论点,其中一些包括(撇开哲学论点):

  • 多重继承太复杂而且经常模棱两可
  • 这是不必要的,因为接口提供了类似的东西
  • 在接口不合适的情况下,组合是一个很好的替代品

我来自 C++ 背景,怀念多重继承的力量和优雅。尽管它并不适合所有软件设计,但在某些情况下,很难否认它对接口、组合和类似的 OO 技术的实用性。

排除多重继承是否意味着开发人员不够聪明,无法明智地使用它们,并且无法在出现复杂性时解决它们?

我个人会欢迎在 C#(可能是 C##)中引入多重继承。


附录:我很想知道来自单一(或程序背景)与多重继承背景的回复。我经常发现没有多重继承经验的开发人员通常会默认使用多重继承是不必要的论点,因为他们没有任何范式经验。

4

34 回答 34

114

我从来没有错过过一次,从来没有。是的,它 [MI] 变得复杂了,是的,接口在很多方面都做着类似的工作——但这​​不是最重要的一点:在一般意义上,大多数时候根本不需要它。在许多情况下,甚至单继承也被过度使用。

于 2008-10-10T14:50:51.977 回答
37

更喜欢聚合而不是继承!

class foo : bar, baz

通常更好地处理

class foo : Ibarrable, Ibazzable
{
  ... 
  public Bar TheBar{ set }
  public Baz TheBaz{ set }

  public void BarFunction()
  {
     TheBar.doSomething();
  }
  public Thing BazFunction( object param )
  {
    return TheBaz.doSomethingComplex(param);
  }
}

通过这种方式,您可以交换 IBarrable 和 IBazzable 的不同实现以创建应用程序的多个版本,而无需编写另一个类。

依赖注入可以帮助解决这个问题。

于 2008-10-10T15:00:16.730 回答
25

处理多重继承的问题之一是接口继承和实现继承之间的区别。

通过使用纯接口,C# 已经有了一个干净的接口继承实现(包括隐式或显式实现的选择)。

如果您查看 C++,对于您在class声明中冒号后指定的每个类,您获得的继承类型由访问修饰符(private、、protectedpublic)确定。使用public继承,你会得到多重继承的全部混乱——多个接口与多个实现混合在一起。通过private继承,您只需获得实现。"" 的对象class Foo : private Bar永远不能传递给期望 a 的函数,Bar因为就好像Foo该类真的只有一个私有Bar字段和一个自动实现的委托模式

纯多重实现继承(实际上只是自动委托)不会出现任何问题,并且在 C# 中会很棒。

至于类的多接口继承,实现该特性有许多不同的可能设计。每种具有多重继承的语言都有自己的规则,即在多个基类中以相同名称调用方法时会发生什么。一些语言,如 Common Lisp(尤其是 CLOS 对象系统)和 Python,有一个元对象协议,您可以在其中指定基类优先级。

这是一种可能性:

abstract class Gun
{ 
    public void Shoot(object target) {} 
    public void Shoot() {}

    public abstract void Reload();

    public void Cock() { Console.Write("Gun cocked."); }
}

class Camera
{ 
    public void Shoot(object subject) {}

    public virtual void Reload() {}

    public virtual void Focus() {}
}

//this is great for taking pictures of targets!
class PhotoPistol : Gun, Camera
{ 
    public override void Reload() { Console.Write("Gun reloaded."); }

    public override void Camera.Reload() { Console.Write("Camera reloaded."); }

    public override void Focus() {}
}

var    pp      = new PhotoPistol();
Gun    gun     = pp;
Camera camera  = pp;

pp.Shoot();                    //Gun.Shoot()
pp.Reload();                   //writes "Gun reloaded"
camera.Reload();               //writes "Camera reloaded"
pp.Cock();                     //writes "Gun cocked."
camera.Cock();                 //error: Camera.Cock() not found
((PhotoPistol) camera).Cock(); //writes "Gun cocked."
camera.Shoot();                //error:  Camera.Shoot() not found
((PhotoPistol) camera).Shoot();//Gun.Shoot()
pp.Shoot(target);              //Gun.Shoot(target)
camera.Shoot(target);          //Camera.Shoot(target)

在这种情况下,只有第一个列出的类的实现在发生冲突时被隐式继承。必须明确指定其他基类型的类才能获得它们的实现。为了使它更防白痴,编译器可以在冲突的情况下禁止隐式继承(冲突的方法总是需要强制转换)。

此外,您现在可以使用隐式转换运算符在 C# 中实现多重继承:

public class PhotoPistol : Gun /* ,Camera */
{
    PhotoPistolCamera camera;

    public PhotoPistol() {
        camera = new PhotoPistolCamera();
    }

    public void Focus() { camera.Focus(); }

    class PhotoPistolCamera : Camera 
    { 
        public override Focus() { }
    }

    public static Camera implicit operator(PhotoPistol p) 
    { 
        return p.camera; 
    }
}

但是,它并不完美,因为isandas运算符和System.Type.IsSubClassOf().

于 2008-10-10T17:36:27.133 回答
15

这是我一直遇到的多重继承的一个非常有用的案例。

作为工具包供应商,我不能更改已发布的 API,否则我将破坏向后兼容性。由此产生的一件事是,一旦我发布了接口,我就永远无法添加它,因为它会破坏任何实现它的人的编译——唯一的选择是扩展接口。

这对现有客户来说很好,但新客户会认为这种层次结构不必要地复杂,如果我从一开始就设计它,我不会选择以这种方式实现它——我必须这样做,否则我将失去向后兼容性. 如果接口是内部的,那么我只需添加它并修复实现者。

在许多情况下,接口的新方法有一个明显而小的默认实现,但我无法提供它。

我更喜欢使用抽象类,然后当我必须添加一个方法时,添加一个具有默认实现的虚拟方法,有时我们会这样做。

当然,问题是如果这个类可能会混入已经在扩展某些东西的东西中——那么我们别无选择,只能使用接口并处理扩展接口。

如果我们认为我们在很大程度上遇到了这个问题,我们会选择一个丰富的事件模型——我认为这可能是 C# 中的正确答案,但并不是每个问题都以这种方式解决——有时你想要一个简单的公共接口,以及更丰富的扩展程序。

于 2008-10-10T15:34:48.753 回答
12

C# 支持单一继承、接口和扩展方法。在它们之间,它们几乎提供了多重继承所提供的一切,而没有多重继承带来的麻烦。

于 2008-10-10T14:55:11.187 回答
8

CLR 不以我所知道的任何方式支持多重继承,所以我怀疑它是否可以像在 C++(或 Eiffel)中那样以有效的方式得到支持,因为该语言是专门设计的为 MI)。

多重继承的一个很好的替代方法称为特征。它允许您将各种行为单元混合到一个类中。编译器可以支持特征作为对单继承类型系统的编译时扩展。您只需声明类 X 包括特征 A、B 和 C,编译器就会将您要求的特征放在一起以形成 X 的实现。

例如,假设您正在尝试实现 IList(of T)。如果您查看 IList(of T) 的不同实现,它们通常共享一些完全相同的代码。这就是特征。您只需声明一个带有公共代码的特征,您就可以在 IList(of T) 的任何实现中使用该公共代码——即使该实现已经有一些其他基类。语法可能如下所示:

/// This trait declares default methods of IList<T>
public trait DefaultListMethods<T> : IList<T>
{
    // Methods without bodies must be implemented by another 
    // trait or by the class
    public void Insert(int index, T item);
    public void RemoveAt(int index);
    public T this[int index] { get; set; }
    public int Count { get; }

    public int IndexOf(T item)
    {
        EqualityComparer<T> comparer = EqualityComparer<T>.Default;
        for (int i = 0; i < Count; i++)
            if (comparer.Equals(this[i], item))
                return i;
        return -1;
    }
    public void Add(T item)
    {
        Insert(Count, item);
    }
    public void Clear()
    {   // Note: the class would be allowed to override the trait 
        // with a better implementation, or select an 
        // implementation from a different trait.
        for (int i = Count - 1; i >= 0; i--)
            RemoveAt(i);
    }
    public bool Contains(T item)
    {
        return IndexOf(item) != -1;
    }
    public void CopyTo(T[] array, int arrayIndex)
    {
        foreach (T item in this)
            array[arrayIndex++] = item;
    }
    public bool IsReadOnly
    {
        get { return false; }
    }
    public bool Remove(T item)
    {
        int i = IndexOf(item);
        if (i == -1)
            return false;
        RemoveAt(i);
        return true;
    }
    System.Collections.IEnumerator 
        System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
    IEnumerator<T> GetEnumerator()
    {
        for (int i = 0; i < Count; i++)
            yield return this[i];
    }
}

你使用这样的特征:

class MyList<T> : MyBaseClass, DefaultListMethods<T>
{
    public void Insert(int index, T item) { ... }
    public void RemoveAt(int index)       { ... }
    public T this[int index] {
        get { ... }
        set { ... }
    }
    public int Count {
        get { ... }
    }
}

当然,我在这里只是触及表面。有关更完整的描述,请参阅论文Traits:Composable Units of Behavior (PDF)。

Rust 语言(来自 Mozilla)以一种有趣的方式实现了 Traits:他们注意到 Traits 类似于默认接口实现,因此他们将接口和 Traits 统一为一个特性(他们称之为 Trait)。trait 和默认接口实现(Java 现在拥有)之间的主要区别在于,trait 可以包含私有或受保护的方法,这与必须是公共的传统接口方法不同。如果traits和interface没有统一为一个特性,那么另一个区别是你可以引用一个接口,但是你不能引用一个trait;特征本身不是类型。

于 2008-10-10T16:35:50.207 回答
7

实际上,由于一个特定原因,我错过了多重继承……处置模式。

每次我需要实现 dispose 模式时,我都会对自己说:“我希望我可以从一个通过一些虚拟覆盖实现 dispose 模式的类派生。” 我将相同的样板代码复制并粘贴到每个实现 IDispose 的类中,我讨厌它。

于 2008-11-24T23:45:55.743 回答
5

YES! YES! and YES!

Seriously, I've been developing GUI libraries my entire career, and MI (Multiple Inheritance) makes this FAR easier than SI (Single Inheritance)

First I did SmartWin++ in C++ (MI heavily used) then I did Gaia Ajax and finally now Ra-Ajax and I can with extreme confident state that MI rules for some places. One of those places being GUI libraries...

And the arguments claiming that MI "is too complex" and such are mostly put there by people trying to construct language wars and happens to belong to the camp which "currently doesn't have MI"...

Just like functional programming languages (like Lisp) have been taught (by the "non-Lispers") as "too complex" by non-functional programming language advocates...

People are afraid of the unknown...

MI RULES!

于 2008-11-25T00:38:35.887 回答
5

仅仅出于您所说的原因,我会反对多重继承。开发人员会滥用它。我已经看到每个类从实用程序类继承的问题已经足够多,因此您可以从每个类调用一个函数而无需输入太多内容,以了解多重继承在许多情况下会导致错误的代码。关于 GoTo 也可以这样说,这是它的使用如此不受欢迎的原因之一。我认为多重继承确实有一些很好的用途,就像 GoTo 一样,在理想的世界中,它们都只在适当的时候使用,不会有问题。然而,世界并不理想,所以我们必须保护糟糕的程序员免受自己的伤害。

于 2008-10-10T14:55:29.300 回答
3

我很高兴 C# 没有多重继承,尽管它有时很方便。相反,我希望看到的是能够提供接口方法的默认实现。那是:

interface I
{
    void F();
    void G();
}


class DefaultI : I
{
    void F() { ... }
    void G() { ... }
}

class C : I = DefaultI
{
    public void F() { ... } // implements I.F
}

在这种情况下,((I)new C()).F()将调用C的实现I.F(),而((I)new C()).G()将调用DefaultI的实现I.G()

在将其添加到语言中之前,语言设计者必须解决许多问题,但没有一个是非常困难的,结果将涵盖使多重继承可取的许多需求。

于 2008-11-07T06:59:37.190 回答
2

不。

(用于投票)

于 2008-10-10T15:25:48.380 回答
2

关于 DataFlex 4GL v3+(我知道,我知道,Data什么?)真正好的和(当时)新颖的事情之一是它对mixin继承的支持——来自任何其他类的方法可以在你的类中重用;只要您的类提供这些方法使用的属性,它就可以正常工作,并且没有“钻石问题”或其他多重继承“陷阱”需要担心。

我希望在 C# 中看到类似的东西,因为它可以简化某些类型的抽象和构造问题

于 2008-11-14T16:00:36.593 回答
2

您可以使用mixins代替多重继承,这是一个更好的解决方案。

于 2008-12-12T14:17:00.180 回答
2

自从它首次作为 alpha/beta 版本发布以来,我一直在使用 C#,并且从未错过多重继承。MI 在某些方面很好,但几乎总是有其他方法可以实现相同的结果(其中一些实际上最终变得更简单或创建更易于理解的实现)。

于 2008-10-10T14:58:53.380 回答
2

多重继承通常很有用,许多 OO 语言以一种或另一种方式实现它(C++、Eiffel、CLOS、Python ......)。是必不可少的吗?不,它很好吗?是的。

于 2008-10-10T15:01:20.847 回答
2

更新
我挑战所有投票给我的人,向我展示任何我无法轻松移植到单继承语言的多继承示例。除非有人可以展示任何此类样本,否则我声称它不存在。我已经将大量的 C++ 代码 (MH) 移植到了 Java (no-MH),这从来都不是问题,无论 C++ 代码使用了多少 MH。


到目前为止,没有人能证明多重继承比您在帖子中提到的其他技术有任何优势(使用接口和委托,我可以在没有太多代码或开销的情况下获得完全相同的结果),但它有几个众所周知的缺点(菱形问题是最烦人的)。

实际上,多重继承通常被滥用。如果您使用 OO 设计以某种方式将现实世界建模为类,那么您将永远无法达到多重继承真正有意义的地步。您能否提供一个有用的多重继承示例?到目前为止,我看到的大多数示例实际上都是“错误的”。他们使某些东西成为子类,这实际上只是一个额外的属性,因此实际上是一个接口。

看看萨瑟。它是一种编程语言,其中接口确实具有多重继承,为什么不(它不会产生菱形问题),但是没有接口的类没有任何继承。它们只能实现接口,并且可以“包含”其他对象,这使得这些其他对象成为它们的固定部分,但这与继承不同,它是一种委托形式(通过包含对象“继承”的方法调用是实际上只是转发到封装在您的对象中的这些对象的实例)。我认为这个概念非常有趣,它表明您可以拥有一个完全干净的 OO 语言,而无需任何实现继承。

于 2008-10-10T15:06:29.950 回答
1

我更喜欢 C++。我使用过 Java、C# 等。随着我的程序在这样的 OO 环境中变得更加复杂,我发现自己缺少多重继承。这是我的主观体验。

它可以制作出令人惊叹的意大利面条代码……它可以制作出令人惊叹的优雅代码。

于 2008-10-10T23:08:36.123 回答
1

I believe languages like C# should give the programmer the option. Just because it maybe too complicated does not mean it will be too complicated. Programming languages should provide the developer with tools to build anything the programmer wants to.

You choose to use those API's a developer already wrote, you dont have too.

于 2008-11-25T00:26:14.603 回答
1

我认为它真的很简单。就像任何其他复杂的编程范例一样,您可能会滥用它并伤害自己。你可以滥用对象吗(哦,是的!),但这并不意味着 OO 本身就是不好的。

与 MI 类似。如果您没有继承类的大型“树”,或者没有许多提供相同命名方法的类,那么您将完全可以使用 MI。事实上,由于 MI 提供了实现,您通常会比 SI 实现更好,因为您必须重新编码或将方法剪切和粘贴到委托对象。在这些情况下,代码越少越好。您可以尝试通过接口继承重用对象,从而将共享代码弄得一团糟。而且这样的变通办法并不正确。

我认为.NET 的单继承模型是有缺陷的:它们应该只使用接口,或者只使用 MI。拥有“对半”(即单个实现继承加上多个接口继承)比它应该的更令人困惑,而且一点也不优雅。

我来自 MI 背景,我并不害怕或被它灼伤。

于 2008-10-10T15:32:02.953 回答
1

我已经在这里发布了几次,但我只是觉得它真的很酷。你可以在这里学习如何伪造 MI 。我还认为这篇文章强调了为什么 MI 会如此痛苦,即使这不是故意的。

我既不想错过也不需要它,我更喜欢用对象的组合来达到我的目的。这也是这篇文章的真正意义所在。

于 2008-10-10T15:32:13.297 回答
1

我自己也在 C++ 中使用了多重继承,但你真的必须知道你在做什么才能不给自己带来麻烦,特别是如果你有两个共享一个祖父母的基类。然后你可能会遇到虚拟继承的问题,必须声明你要调用的每个构造函数(这使得二进制重用变得更加困难)......这可能是一团糟。

更重要的是,当前 CLI 的构建方式使 MI 无法轻松实现。我敢肯定,如果他们愿意,他们可以做到,但我有其他的东西我宁愿在 CLI 中看到,而不是多重继承。

我希望看到的东西包括Spec#的一些特性,比如不可为空的引用类型。我还希望通过能够将参数声明为 const 以及声明函数 const 来看到更多的对象安全性(这意味着您要保证对象的内部状态不会被方法和编译器会仔细检查你)。

我认为在单继承、多接口继承、泛型和扩展方法之间,您几乎可以做任何您需要做的事情。如果有什么东西可以为渴望 MI 的人带来改善,我认为需要某种语言结构来简化聚合和组合。这样,您可以拥有一个共享接口,然后将您的实现委托给您通常继承的类的私有实例。现在,这需要大量的样板代码来完成。拥有更自动化的语言功能将有很大帮助。

于 2008-10-10T15:48:06.200 回答
1

一位同事写了这篇博客,讲述如何在 C# 中使用动态编译获得多重继承之类的东西:

http://www.atalasoft.com/cs/blogs/stevehawley/archive/2008/09/29/late-binding-in-c-using-dynamic-compilation.aspx

于 2008-10-10T15:22:21.320 回答
1

我认为如果没有提供足够的投资回报率,它会使事情变得过于复杂。我们已经看到人们使用过深的继承树来屠杀 .NET 代码。如果人们有权进行多重继承,我可以想象会发生怎样的暴行。

我不会否认它有潜力,但我只是没有看到足够的好处。

于 2008-10-10T14:55:05.930 回答
1

虽然在某些情况下它确实有用,但我发现大多数时候我认为我需要它,但我真的不需要。

于 2008-10-10T14:56:59.110 回答
1

给 C#隐式,你不会错过多重继承,或任何继承。

于 2008-12-12T14:07:42.680 回答
1

不,我不。我使用所有其他 OO 功能来开发我想要的东西。我使用接口和对象封装,我从不限制我想做的事情。

于 2008-12-12T14:09:44.783 回答
1

我尽量不使用继承。每次我能做的越少。

于 2008-12-12T14:10:28.463 回答
1

不,我们远离它。你现在确实需要它。

于 2008-10-10T22:35:34.520 回答
1

除非钻石问题得到解决,否则不会。你可以使用组合,直到这个问题没有解决。

于 2010-02-18T06:57:00.737 回答
0

我说不,直到钻石问题(类的多重继承不好的一个重要原因)得到充分解决并且解决方案与使用接口一样好。简而言之,钻石问题基本上与由于类的多重继承而导致类中的数据、方法和事件的潜在歧义有关。

P/S 你“很少”避免一个你急需的编程解决方案,因为它很难。“困难”不是没有多重继承的借口。多线程很难,但它在 C# 中可用。没有多重继承的一大原因是钻石问题(http://en.wikipedia.org/wiki/Diamond_problem)。一个类似的推理适用于您对更好的替代方案的评论(人们以不同的方式解决和思考,所以有时一个解决方案是一个坏主意)和合适的选项已经存在(为什么在 ADO.NET 成功并且成熟时创建 LINQ...由于以一个比另一个强。)

于 2009-10-09T22:35:51.263 回答
0

如果我们引入多重继承,那么我们将再次面临 C++ 的老钻石问题......

但是对于那些认为不可避免的人,我们仍然可以通过组合来引入多重继承效应(在一个对象中组合多个对象并公开将职责委托给组合对象并返回的公共方法)......

那么,为什么还要费心进行多重继承并使您的代码容易受到不可避免的异常的影响......

于 2009-05-29T11:42:28.717 回答
0

如果接口可以实现,它将使事情变得更容易,更少的代码。但是它们会被误用而不是正确使用吗?我认为,在您看到如何正确完成之前,您不会错过它。我对多重继承不安全的论点感到恼火。它不是。但几乎所有的语言实现都是。但我们需要它吗?,我不知道。

我更喜欢返回类型协方差,这可以很容易地添加(只需放松规则,在覆盖方法的返回类型上),并且总是安全的。

例子:

class shape {}

class circle : shape {}

interface part {
    shape Form();
}

interface wheel : part {
    circle Form();
}
于 2011-02-02T17:05:34.243 回答
0

我几乎不会错过 C# 中的多重继承。

如果您使用多重继承来定义实际的域模型,那么根据我的经验,您通常可以使用单一继承和一些好的设计模式来创建同样好的设计。

我发现多重继承具有真正价值的大多数地方不是域本身,而是一些需要您使用 MI 的技术/框架约束。使用 ATL 实现 COM 对象是一个很好的例子,说明您使用多重继承来实现 COM 对象所需的所有必要接口,并且是对丑陋(与 .NET 相比)技术的优雅解决方案。从它是一个 C++ 框架的角度来看是优雅的!

不过,我现在有一种情况,我可以使用多重继承。我有一个类需要从其他域对象派生功能,但它也需要可用于跨 AppDomain 调用。这意味着它必须从 MarshalByRefObject 继承。所以在这种特殊情况下,我真的很想从 MarshalByRefObject 和我的特定域对象中派生。然而这是不可能的,所以我的类必须实现与我的域对象相同的接口,并将调用转发到聚合实例。

但正如我所说,这是技术/框架施加约束的情况,而不是域模型本身。

于 2009-09-11T08:56:43.343 回答
-2

接口是多重继承。事实上,我认为 Java/C# 类型接口是多重继承的“正确”实现。通过使用接口来强制进行多重继承,而不是允许从多个具体或抽象类进行继承,这会迫使开发人员使用组合/委托来进行代码重用。继承永远不应该用于代码重用,并且 C++ 类型多重继承的缺失迫使开发人员想出更好的设计类。

于 2008-10-10T23:16:52.673 回答