C#应该有多重继承吗?

时间:2008-10-10 14:49:58

标签: c# inheritance multiple-inheritance

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

  • 多重继承过于复杂且经常含糊不清
  • 这是不必要的,因为接口提供了类似的东西
  • 组合是接口不合适的好替代品

我来自C ++背景,错过了多重继承的力量和优雅。虽然它不适合所有软件设计,但有些情况下很难否认它在接口,组合和类似的OO技术方面的实用性。

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

我个人欢迎将多重继承引入C#(也许是C ##)。


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

34 个答案:

答案 0 :(得分:113)

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

答案 1 :(得分: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的不同实现来创建应用程序的多个版本,而无需编写另一个类。

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

答案 2 :(得分:25)

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

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

如果查看C ++,对于class声明中冒号后指定的每个类,您获得的继承类型由访问修饰符(privateprotected决定,或public)。使用public继承,您可以获得多重继承的完全混乱 - 多个接口与多个实现混合在一起。使用private继承,您只需实现。 “class Foo : private Bar”的对象永远不会传递给期望Bar的函数,因为它就好像Foo类实际上只有一个私有Bar字段并且是自动的 - 实施delegation pattern

纯多重实现继承(实际上只是自动委托)不存在任何问题,并且在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; 
    }
}

但这并不完美,因为 is as 运算符以及System.Type.IsSubClassOf()不支持。< / p>

答案 3 :(得分:15)

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

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

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

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

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

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

如果我们认为我们有很大的问题,我们选择一个丰富的事件模型 - 我认为这可能是C#中的正确答案,但不是每个问题都以这种方式解决 - 有时你想要一个简单的公共界面,以及更丰富的扩展器。

答案 4 :(得分:12)

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

答案 5 :(得分:8)

我知道CLR不支持多重继承,因此我怀疑它是否能够以有效的方式得到支持,就像它在C ++中一样(或者Eiffel,考虑到语言,它可以做得更好)是专门为MI设计的。)

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

例如,假设您正在尝试实现IList(T)。如果你看一下IList(T)的不同实现,它们通常会共享一些完全相同的代码。这就是特征。你只需要在其中声明一个包含公共代码的特征,并且可以在IList(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统一为单个功能(他们称之为traits)。 traits和默认接口实现(Java现在拥有)之间的主要区别在于traits可以包含private或protected方法,这与传统的必须公开的接口方法不同。如果特征和界面统一到一个特征中,那么另一个区别是你可以引用一个接口,但你不能引用一个特征;特质本身不是一种类型。

答案 6 :(得分:7)

由于某个特定原因,我实际上错过了多重继承......处理模式。

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

答案 7 :(得分:5)

我会因为你陈述的原因而反对多重继承。开发人员会滥用它。我已经看到从实用程序类继承的每个类都有足够的问题,因此你可以从每个类调用一个函数而不需要输入那么多,知道多重继承会在很多情况下导致错误的代码。关于GoTo可以说同样的事情,这也是它使用的原因之一。我认为多重继承确实有一些很好的用途,就像GoTo一样。在一个理想的世界里,只有在恰当的时候使用它们才会有问题。然而,世界并不理想,所以我们必须保护坏程序员不受其影响。

答案 8 :(得分:5)

YES!是!是的!

说真的,我整个职业生涯都在开发GUI库,MI(多重继承)使这个FAR比SI(单一继承)更容易

首先我在C ++中使用了SmartWin++(MI使用率很高)然后我做了盖亚阿贾克斯,最后我现在Ra-Ajax我可以非常自信地说MI管理某些地方。其中一个地方是GUI库...

声称MI“过于复杂”的论点大多是人们试图构建语言战并且恰好属于“目前没有MI”的阵营......

就像函数式编程语言(如Lisp)被非函数式编程语言倡导者所教导(“非Lispers”)一样“过于复杂”......

人们害怕未知......

MI RULES!

答案 9 :(得分: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的{​​{1}}实现}。

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

答案 10 :(得分:2)

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

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

答案 11 :(得分:2)

(投票)

答案 12 :(得分:2)

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

答案 13 :(得分:2)

<强>更新
我向所有投票给我的人发起挑战,向我展示任何多重继承的例子,我不能轻易地将其移植到具有单一继承的语言。除非有人能展示任何此类样本,否则我声称它不存在。我已经将大量的C ++代码(MH)移植到Java(no-MH),无论C ++代码使用了多少MH,这都不是问题。


到目前为止,没有人能证明多重继承比您在帖子中提到的其他技术any advantage更多(使用接口和委托,我可以得到完全相同的结果而没有太多的代码或开销),而它有一对众所周知的缺点(diamond problem是最烦人的)。

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

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

答案 14 :(得分:2)

一般来说,多重继承可能很有用,许多OO语言都是以这种或那种方式实现的(C ++,Eiffel,CLOS,Python ......)。这是必要的吗?没有。好吗?是。

答案 15 :(得分:2)

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

答案 16 :(得分:1)

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

答案 17 :(得分:1)

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

答案 18 :(得分:1)

提供C#implicits,您将不会错过多重继承或任何继承。

答案 19 :(得分:1)

我相信像C#这样的语言应该为程序员提供选择。仅仅因为可能过于复杂并不意味着过于复杂。编程语言应该为开发人员提供构建程序员想要的任何东西的工具。

您选择使用开发人员已编写的API,您也没有。

答案 20 :(得分:1)

否,除非钻石问题得到解决。并且你可以使用构图,直到这没有解决。

答案 21 :(得分:1)

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

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

答案 22 :(得分:1)

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

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

答案 23 :(得分:1)

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

答案 24 :(得分:1)

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

更重要的是,当前构建CLI的方式阻止了MI 轻松实现。我确信如果他们愿意的话他们可以做到,但我还有其他的东西,我宁愿在CLI中看到,而不是多重继承。

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

我认为在单继承,多接口继承,泛型和扩展方法之间,你可以做任何你需要的事情。如果有任何事情可以改善想要MI的人的事情,我认为某种语言构造可以允许更容易的聚合和组合。这样你就可以拥有一个共享接口,但是然后将你的实现委托给你通常会继承的类的私有实例。现在,这需要很多锅炉板代码。拥有更加自动化的语言功能将有很大帮助。

答案 25 :(得分:1)

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

我既不想念它也不需要它,我更喜欢使用物体的组合来实现我的目的。这也是文章的重点。

答案 26 :(得分:1)

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

与MI类似。如果你没有一个继承类的大型“树”,或者许多提供相同命名方法的类,那么你将完全可以使用MI。实际上,由于MI提供了实现,因此您通常会比SI实现更好,在SI实现中您必须重新编码或剪切和粘贴方法到委托对象。在这些情况下,代码越少越好。通过尝试通过接口继承重用对象,可以使共享代码变得非常混乱。这样的解决方法闻起来不对。

我认为.NET的单继承模型存在缺陷:它们应该仅使用接口,或仅使用MI。 “一半和一半”(即单个实现继承加上多个接口继承)比它应该更加混乱,而且根本不优雅。

我来自MI背景,我并不害怕或被它烧毁。

答案 27 :(得分:1)

一位同事写了这篇博客,介绍如何在C#中使用动态编译获得多重继承:

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

答案 28 :(得分:1)

虽然确实存在可能有用的情况,但我发现大部分时间我觉得自己需要它,但事实并非如此。

答案 29 :(得分:0)

如果我们引入多重继承,那么我们将再次面对C ++的旧Diamond问题......

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

那么为什么还要花费多次继承并使你的代码容易受到不可避免的异常的影响......

答案 30 :(得分:0)

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

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

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

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

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

答案 31 :(得分:0)

我说没有,直到钻石问题(一个很大的原因,为什么多重继承与类是坏的)得到充分解决,如果解决方案与使用接口一样好。简而言之,钻石问题基本上与由于通过类的多重继承而在类中的数据,方法和事件中的潜在歧义有关。

P / S你“很少”避免一个你非常需要的编程解决方案,因为它很难。 “难度”不是没有多重遗产的借口。多线程很难在C#中使用。没有多重遗传的大问题是钻石问题(http://en.wikipedia.org/wiki/Diamond_problem)。一个熟悉的推理适用于你关于更好的替代方案的评论(人们以不同的方式解决和思考,所以有时候一个解决方案是一个坏主意)并且已经存在合适的选项(为什么在ADO.NET完成技巧并且成熟时创建LINQ ...到期一个人的力量超过另一个人。)

答案 32 :(得分:0)

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

我更喜欢返回类型协方差,这可以轻松添加(只需放宽规则,在过度使用的方法上返回类型),并且始终是安全的。

示例:

class shape {}

class circle : shape {}

interface part {
    shape Form();
}

interface wheel : part {
    circle Form();
}

答案 33 :(得分:-2)

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