C# 是否应该具有多重继承

本文关键字:多重继承 是否 | 更新日期: 2023-09-27 17:48:53

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

  • 多重继承过于复杂,而且往往模棱两可
  • 这是不必要的,因为接口提供类似的东西
  • 组合是界面不合适的一个很好的替代品

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

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

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


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

C# 是否应该具有多重继承

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

更喜欢聚合而不是继承!

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 的不同实现,以创建应用程序的多个版本,而无需编写另一个类。

依赖注入可以对此有很大帮助。

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

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

如果你看一下C++,对于你在class声明中冒号后面指定的每个类,你得到的继承类型由访问修饰符(privateprotectedpublic(决定。通过public继承,您可以获得多重继承的全部混乱 - 多个接口与多个实现混合在一起。通过private继承,您只需获得实现即可。"class Foo : private Bar"的对象永远不能传递给需要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; 
    }
}
但是,它

并不完美,因为isas运算符不支持它,并且System.Type.IsSubClassOf() .

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

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

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

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

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

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

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

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

据我所知,CLR 不支持多重继承,因此我怀疑它能否像在 C++(或 Eiffel,鉴于该语言是专门为 MI 设计的(中那样有效地支持它。

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

例如,假设您正在尝试实现 IList(of T(。如果你看一下IList(of T(的不同实现,它们通常共享一些完全相同的代码。这就是特质的出现。您只需声明一个包含公共代码的 trait,您就可以在 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 { ... }
    }
}

当然,我只是在这里触及表面。有关更完整的描述,请参阅论文特征:可组合的行为单位 (PDF(。

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

我实际上因为一个特定原因错过了多重继承......释放模式。

每次我需要实现释放模式

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

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

是的!是的!是的!

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

首先我在C++年做了SmartWin++(MI大量使用(,然后我做了Gaia Ajax,最后是现在的Ra-Ajax,我可以非常自信地表示MI统治某些地方。其中一个地方是 GUI 库...

而声称MI"太复杂"之类的论点大多是由试图构建语言战争的人提出的,并且恰好属于"目前没有MI"的阵营......

就像函数式

编程语言(如Lisp(被非函数式编程语言倡导者教导(由"非Lispers"("太复杂"一样......

人们害怕未知...

MI规则!

我很高兴

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()会调用CI.F()实现,而((I)new C()).G()会调用DefaultII.G()实现。

在将其

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

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

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

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


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

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

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

No.

(用于投票(

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

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

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

我认为如果没有提供足够的投资回报率,它会使事情过于复杂。我们已经看到人们使用太深的继承树来屠杀.NET代码。我可以想象,如果人们有能力继承多重遗产,那将是多么的暴行。

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

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

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

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

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

与心肌梗死类似。如果你没有一大堆继承的类"树",或者很多提供相同命名方法的类,那么你对MI来说就完全没问题了。事实上,由于 MI 提供了实现,因此您通常比必须重新编码或将方法剪切和粘贴到委派对象的 SI 实现更好。在这些情况下,代码越少越好。您可以通过尝试通过接口继承重用对象来使共享代码变得一团糟。而且这样的解决方法闻起来不对。

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

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

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

既不怀念也不需要它,我更喜欢使用对象的构图来实现我的目的。 这也是本文的重点。

我也在

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

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

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

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

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

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

我相信

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

选择使用开发人员已经编写的那些API,你也没有。

提供 C# 隐式,您不会错过多重继承或任何与此相关的继承。

不,

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

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

否,

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

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

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

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

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