C#中的多重继承


212

由于多重继承很糟糕(这会使源代码更加复杂),因此C#不会直接提供这种模式。但是有时具有此功能会有所帮助。

例如,我能够使用接口和三个类似的类来实现缺少的多继承模式:

public interface IFirst { void FirstMethod(); }
public interface ISecond { void SecondMethod(); }

public class First:IFirst 
{ 
    public void FirstMethod() { Console.WriteLine("First"); } 
}

public class Second:ISecond 
{ 
    public void SecondMethod() { Console.WriteLine("Second"); } 
}

public class FirstAndSecond: IFirst, ISecond
{
    First first = new First();
    Second second = new Second();
    public void FirstMethod() { first.FirstMethod(); }
    public void SecondMethod() { second.SecondMethod(); }
}

每次我向一个接口添加一个方法时,我都需要更改类FirstAndSecond

有没有办法像C ++中那样将多个现有类注入到一个新类中?

也许有使用某种代码生成的解决方案?

或者它可能看起来像这样(虚构的C#语法):

public class FirstAndSecond: IFirst from First, ISecond from Second
{ }

这样,当我修改其中一个接口时,就不需要更新FirstAndSecond类。


编辑

也许最好考虑一个实际的例子:

您有一个现有的类(例如,基于ITextTcpClient的基于文本的TCP客户端),您已经在项目中的不同位置使用了该类。现在您感到需要创建类的组件,以便Windows窗体开发人员可以轻松访问。

据我所知,您目前有两种方法可以做到这一点:

  1. 编写一个从组件继承的新类,并使用该类本身的实例实现TextTcpClient类的接口,如FirstAndSecond所示。

  2. 编写一个继承自TextTcpClient的新类,并以某种方式实现IComponent(实际上尚未尝试过)。

在这两种情况下,您都需要按方法而不是按类进行工作。既然您知道我们将需要TextTcpClient和Component的所有方法,那么将这两种方法组合为一个类将是最简单的解决方案。

为了避免冲突,可以通过代码生成来完成,在代码生成后可以更改结果,但是手动键入此内容纯属麻烦。


在某种程度上,这不仅仅是变相的多重继承,它又怎么那么复杂呢?
哈波

考虑到3.5中的新扩展方法及其工作方式(生成静态成员调用),这可能是下一种.NET语言演变。
拉里

有时我想知道为什么人们不只是... A类:B类:C类?
Chibueze Opata 2012年

@NazarMerza:链接已更改。现在:具有多重继承的问题
Craig McQueen 2013年

9
不要让宣传愚弄你。您的示例显示了多重继承很有用,而接口只是缺乏继承的一种解决方法
Kemal Erdogan 2013年

Answers:


125

由于多重继承很糟糕(这会使源代码更加复杂),因此C#不会直接提供这种模式。但是有时具有此功能会有所帮助。

C#和.net CLR尚未实现MI,因为他们尚未得出结论,认为MI将如何在C#,VB.net和其他语言之间进行互操作,不是因为“它将使源代码更加复杂”。

MI是一个有用的概念,未解决的问题是:-“如果在不同的超类中有多个公共基类,您将怎么办?

Perl是我曾经使用过的唯一语言,在MI上可以很好地工作。.Net可能有一天会引入它,但目前还没有,CLR已经支持MI,但是正如我所说的那样,目前还没有语言构造。

在此之前,您只能使用代理对象和多个接口:(


39
CLR不支持多重实现继承,而仅支持多重接口继承(C#也支持)。
Jordão酒店

4
@Jordão:出于完整性考虑:编译器可以在CLR中为其类型创建MI。它确实有一些警告,例如,它不符合CLS。欲了解更多信息,请参阅本(2004)的文章blogs.msdn.com/b/csharpfaq/archive/2004/03/07/...
dvdvorle

2
@MrHappy:非常有趣的文章。我实际上已经研究了一些C#的特征组合方式,看看吧。
Jordão酒店

10
@MandeepJanjua我没有要求任何此类事情,我说“可以介绍一下”。事实仍然是ECMA标准CLR确实为多重继承提供了IL机制,只是没有任何东西可以充分利用它。
IanNorton

4
仅供参考,多重继承也不错,也不会使代码变得那么复杂。只是以为我会提到它。
Dmitri Nesteruk '17

214

考虑只使用组合而不是尝试模拟多重继承。您可以使用Interfaces定义组成组合的类,例如:ISteerable隐含类型的属性SteeringWheelIBrakable隐含类型的属性BrakePedal,等等。

完成此操作后,可以使用C#3.0中添加的扩展方法功能来进一步简化对这些隐含属性的调用方法,例如:

public interface ISteerable { SteeringWheel wheel { get; set; } }

public interface IBrakable { BrakePedal brake { get; set; } }

public class Vehicle : ISteerable, IBrakable
{
    public SteeringWheel wheel { get; set; }

    public BrakePedal brake { get; set; }

    public Vehicle() { wheel = new SteeringWheel(); brake = new BrakePedal(); }
}

public static class SteeringExtensions
{
    public static void SteerLeft(this ISteerable vehicle)
    {
        vehicle.wheel.SteerLeft();
    }
}

public static class BrakeExtensions
{
    public static void Stop(this IBrakable vehicle)
    {
        vehicle.brake.ApplyUntilStop();
    }
}


public class Main
{
    Vehicle myCar = new Vehicle();

    public void main()
    {
        myCar.SteerLeft();
        myCar.Stop();
    }
}

13
但这就是要点-这样的想法会简化构图。
乔恩·斯基特

9
是的,但是在某些用例中,您确实需要将方法作为主要对象的一部分
David Pierre

9
不幸的是,成员变量数据无法在扩展方法中访问,因此您必须将其公开为内部或(ug)公共,尽管我认为按合同组合是解决多重继承的最佳方法。
cfeduke

4
很好的答案!简洁,易于理解,非常有用的插图。谢谢!
AJ。

3
我们可能想myCar在致电之前检查是否已经完成转向Stop。如果它可能翻身Stop应用,而myCar在超速。:D
Devraj Gadhavi 2015年

16

我创建了一个C#后编译器来启用这种功能:

using NRoles;

public interface IFirst { void FirstMethod(); }
public interface ISecond { void SecondMethod(); }

public class RFirst : IFirst, Role {
  public void FirstMethod() { Console.WriteLine("First"); }
}

public class RSecond : ISecond, Role {
  public void SecondMethod() { Console.WriteLine("Second"); }
}

public class FirstAndSecond : Does<RFirst>, Does<RSecond> { }

您可以将后编译器作为Visual Studio后生成事件运行:

C:\ some_path \ nroles-v0.1.0-bin \ nutate.exe“ $(TargetPath)”

在同一程序集中,您可以这样使用它:

var fas = new FirstAndSecond();
fas.As<RFirst>().FirstMethod();
fas.As<RSecond>().SecondMethod();

在另一个程序集中,您可以这样使用它:

var fas = new FirstAndSecond();
fas.FirstMethod();
fas.SecondMethod();

6

您可以有一个实现IFirst和ISecond的抽象基类,然后仅从该基类继承。


这可能是最好的解决方案,但不一定是最好的主意:p
leppie

1
向接口添加方法时,您还不需要编辑抽象类吗?
瑞克(Rik)

里克:当你只需要做一次的时候,你到底有多懒?
嬉皮

3
@leppie-“每次我向其中一个接口添加一个方法时,我也需要更改类FirstAndSecond。” 此解决方案无法解决原始问题的这一部分,对吗?
Rik

2
您将不得不编辑抽象类,但不必编辑依赖于它的任何其他类。责任制止于此,而不是继续级联到整个班级。
Joel Coehoorn

3

MI还不错,每个(认真)使用它的人都喜欢它,并且它不会使代码复杂化!至少没有其他构造可能会使代码复杂化。错误代码是错误代码,无论图片中是否存在MI。

无论如何,我有一个我想分享的关于多重继承的不错的小解决方案。http://ra-ajax.org/lsp-liskov-substitution-principle-to-be-or-not-to-be.blog,也可以按照我的签名中的链接... :)


是否可以具有多个继承,并且在进行上下转换时保留身份?对于多重继承问题,我所知道的解决方案围绕着具有不保留身份myFoo的类型转换(如果is是type Foo,其继承自Mooand Goo,两者都继承自Boo,那么(Boo)(Moo)myFoo并且(Boo)(Goo)myFoo将不等效)。您是否知道任何保留身份的方法?
supercat

1
您可以使用web.archive.org或类似的链接来访问该链接,但这实际上是对此处原始问题中提供的解决方案的更详细的讨论。
MikeBeaton

2

在我自己的实现中,我发现使用MI的类/接口虽然是“好的形式”,但往往过于复杂,因为您只需要为几个必要的函数调用设置所有多重继承,就我而言,实际上需要重复执行数十次。

取而代之的是,简单地将静态的“调用函数的函数调用不同的模块化品种”简化为一种OOP替代。我正在研究的解决方案是RPG的“咒语系统”,其中效果需要大量混合和匹配函数调用,以提供多种多样的咒语而无需重新编写代码,就像该示例似乎表明的那样。

现在,大多数功能可以是静态的,因为我不需要拼写逻辑的实例,而类继承在静态时甚至不能使用虚拟或抽象关键字。接口根本无法使用它们。

IMO似乎使编码更快,更干净。如果您只是在做函数,不需要继承的属性,请使用函数。


2

现在,使用C#8,您几乎可以通过接口成员的默认实现实现多重继承:

interface ILogger
{
    void Log(LogLevel level, string message);
    void Log(Exception ex) => Log(LogLevel.Error, ex.ToString()); // New overload
}

class ConsoleLogger : ILogger
{
    public void Log(LogLevel level, string message) { ... }
    // Log(Exception) gets default implementation
}

4
是的,但是请注意,在上面,您将无法执行new ConsoleLogger().Log(someEception)—它根本无法工作,您必须ILogger使用默认接口方法将对象显式转换为。因此,其用途受到一定限制。
Dmitri Nesteruk

1

如果您可以忍受这样的限制,即IFirst和ISecond的方法只能与IFirst和ISecond的契约进行交互(就像您的示例中一样)...您可以使用扩展方法来执行您所要求的。实际上,这种情况很少发生。

public interface IFirst {}
public interface ISecond {}

public class FirstAndSecond : IFirst, ISecond
{
}

public static MultipleInheritenceExtensions
{
  public static void First(this IFirst theFirst)
  {
    Console.WriteLine("First");
  }

  public static void Second(this ISecond theSecond)
  {
    Console.WriteLine("Second");
  }
}

///

public void Test()
{
  FirstAndSecond fas = new FirstAndSecond();
  fas.First();
  fas.Second();
}

因此,基本思想是,您可以在接口中定义所需的实现...此所需的内容应支持扩展方法中的灵活实现。每当您需要“向接口添加方法”时,您都可以添加扩展方法。


1

是的,使用Interface很麻烦,因为每当我们在类中添加方法时,都必须在接口中添加签名。另外,如果我们已经有一个带有一堆方法但没有接口的类,该怎么办?我们必须为要继承的所有类手动创建Interface。最糟糕的是,如果子类要从多接口继承,则必须在子类的Interfaces中实现所有方法。

通过遵循Facade设计模式,我们可以使用以下方法模拟从多个类的继承 访问。在需要继承的类中使用{get; set;}将这些类声明为属性,并且所有公共属性和方法都来自该类,并且在子类的构造函数中实例化父类。

例如:

 namespace OOP
 {
     class Program
     {
         static void Main(string[] args)
         {
             Child somechild = new Child();
             somechild.DoHomeWork();
             somechild.CheckingAround();
             Console.ReadLine();
         }
     }

     public class Father 
     {
         public Father() { }
         public void Work()
         {
             Console.WriteLine("working...");
         }
         public void Moonlight()
         {
             Console.WriteLine("moonlighting...");
         }
     }


     public class Mother 
     {
         public Mother() { }
         public void Cook()
         {
             Console.WriteLine("cooking...");
         }
         public void Clean()
         {
             Console.WriteLine("cleaning...");
         }
     }


     public class Child 
     {
         public Father MyFather { get; set; }
         public Mother MyMother { get; set; }

         public Child()
         {
             MyFather = new Father();
             MyMother = new Mother();
         }

         public void GoToSchool()
         {
             Console.WriteLine("go to school...");
         }
         public void DoHomeWork()
         {
             Console.WriteLine("doing homework...");
         }
         public void CheckingAround()
         {
             MyFather.Work();
             MyMother.Cook();
         }
     }


 }

通过这种结构,Child类可以访问父级和父级的所有方法和属性,从而模拟多重继承并继承父类的实例。不太一样,但是很实用。


2
我不同意第一段。您只需将每个类中所需的方法的签名添加到接口。但是您可以根据需要向任何类添加尽可能多的其他方法。此外,还有一个右键单击提取接口,这使得提取接口的工作变得简单。最后,您的示例绝不是继承(多重或其他方式),而是一个很好的组合示例。las,如果您使用接口还通过构造函数/属性注入来演示DI / IOC,那将有更多好处。虽然我不会投反对票,但我认为这不是一个好的答案。
弗朗西斯·罗杰斯

1
一年后回头看这个线程,我同意您可以在类中添加任意数量的方法而无需在接口中添加签名,但是,这会使接口不完整。此外,我无法在我的IDE中找到右键单击-提取接口,也许我缺少了一些东西。但是,我更大的担心是,当您在接口中指定签名时,继承的类必须实现该签名。我认为这是双重工作,可能导致代码重复。
瑜伽士

提取接口:右键单击类签名,然后提取接口...在VS2015中,相同的过程,只是必须右键单击,然后选择Quick Actions and Refactorings...这是必须知道的,它将为您节省很多时间
Chef_Code

1

我们大家似乎都在用这种方法来简化接口的路径,但是这里明显的另一种可能性是做OOP应该做的事情,并建立您的继承树... (这不是类设计的全部吗?关于?)

class Program
{
    static void Main(string[] args)
    {
        human me = new human();
        me.legs = 2;
        me.lfType = "Human";
        me.name = "Paul";
        Console.WriteLine(me.name);
    }
}

public abstract class lifeform
{
    public string lfType { get; set; }
}

public abstract class mammal : lifeform 
{
    public int legs { get; set; }
}

public class human : mammal
{
    public string name { get; set; }
}

这种结构提供了可重用的代码块,当然,应该如何编写OOP代码?

如果这种方法不太适合我们,我们只需根据所需对象创建新类即可。

class Program
{
    static void Main(string[] args)
    {
        fish shark = new fish();
        shark.size = "large";
        shark.lfType = "Fish";
        shark.name = "Jaws";
        Console.WriteLine(shark.name);
        human me = new human();
        me.legs = 2;
        me.lfType = "Human";
        me.name = "Paul";
        Console.WriteLine(me.name);
    }
}

public abstract class lifeform
{
    public string lfType { get; set; }
}

public abstract class mammal : lifeform 
{
    public int legs { get; set; }
}

public class human : mammal
{
    public string name { get; set; }
}

public class aquatic : lifeform
{
    public string size { get; set; }
}

public class fish : aquatic
{
    public string name { get; set; }
}

0

多重继承是通常引起更多问题而无法解决的问题之一。在C ++中,它很适合给您足够的绳索来挂自己,但Java和C#选择了不给您选择的更安全的方法。最大的问题是,如果您继承了多个类,这些类的方法具有与被继承者不实现的相同签名,则该方法。应该选择哪个类的方法?还是不应该编译?通常,还有另一种方法可以实现大多数不依赖多重继承的事情。


8
请不要用C ++来判断MI,这就像用PHP来判断OOP或用Pintos来判断汽车。这个问题很容易解决:在Eiffel中,从类继承时,还必须指定要继承的方法,然后可以重命名它们。那里没有歧义,也没有惊奇。
约尔格W¯¯米塔格

2
@mP:不,Eiffel提供了真正的多重实现继承。重命名并不意味着失去继承链,也不会失去类的可转换性。
亚伯2010年

0

如果X继承自Y,则具有两个正交的效果:

  1. Y将为X提供默认功能,因此X的代码只需包含与Y不同的内容。
  2. 几乎在任何可以预期到Y的地方,都可以使用X代替。

尽管继承提供了这两个功能,但不难想象在没有另一个功能的情况下可以使用两个功能的情况。我所知道的.net语言没有直接实现第一个而没有第二个的直接方法,尽管可以通过定义一个从不直接使用的基类并拥有一个或多个直接从其继承而无需添加任何东西的类来获得这种功能。新类(此类可以共享所有代码,但不能互相替代)。但是,任何符合CLR的语言都将允许使用提供接口的第二个功能(可替换性)的接口,而无需提供第一个功能(成员重用)。


0

我知道我知道即使它不被允许等等,有时候您实际上需要它,所以对于那些人:

class a {}
class b : a {}
class c : b {}

就像我的情况一样,我想做此类b:Form(是windows.forms)class c:b {}

因为功能的一半是相同的,并且必须通过接口将它们全部重写


1
您的示例未描述多重继承,那么要解决什么问题?一个真正的多重继承示例将显示class a : b, c(实现任何必要的契约漏洞)。也许您的示例过于简化了?
M.Babcock

0

由于多重继承(MI)问题有时会弹出,因此我想添加一种方法来解决合成模式中的一些问题。

我建立在IFirstISecondFirstSecondFirstAndSecond的做法,因为它是在这个问题提出的。我将示例代码减少为IFirst,因为无论接口/ MI基类的数量如何,模式都保持不变。

让我们假设,对于MI FirstSecond,它们都将从同一个基类派生BaseClass,仅使用来自的公共接口元素BaseClass

这可以通过BaseClassFirstSecond实现中添加对的容器引用来表示:

class First : IFirst {
  private BaseClass ContainerInstance;
  First(BaseClass container) { ContainerInstance = container; }
  public void FirstMethod() { Console.WriteLine("First"); ContainerInstance.DoStuff(); } 
}
...

BaseClass引用受保护的接口元素时First,或者当并且Second将其作为MI中的抽象类时,事情变得更加复杂,要求它们的子类实现一些抽象部分。

class BaseClass {
  protected void DoStuff();
}

abstract class First : IFirst {
  public void FirstMethod() { DoStuff(); DoSubClassStuff(); }
  protected abstract void DoStuff(); // base class reference in MI
  protected abstract void DoSubClassStuff(); // sub class responsibility
}

C#允许嵌套类访问其包含类的受保护/私有元素,因此可用于链接实现中的抽象位First

class FirstAndSecond : BaseClass, IFirst, ISecond {
  // link interface
  private class PartFirst : First {
    private FirstAndSecond ContainerInstance;
    public PartFirst(FirstAndSecond container) {
      ContainerInstance = container;
    }
    // forwarded references to emulate access as it would be with MI
    protected override void DoStuff() { ContainerInstance.DoStuff(); }
    protected override void DoSubClassStuff() { ContainerInstance.DoSubClassStuff(); }
  }
  private IFirst partFirstInstance; // composition object
  public FirstMethod() { partFirstInstance.FirstMethod(); } // forwarded implementation
  public FirstAndSecond() {
    partFirstInstance = new PartFirst(this); // composition in constructor
  }
  // same stuff for Second
  //...
  // implementation of DoSubClassStuff
  private void DoSubClassStuff() { Console.WriteLine("Private method accessed"); }
}

涉及很多样板,但是如果FirstMethod和SecondMethod的实际实现足够复杂并且所访问的私有/受保护方法的数量适中,则此模式可能有助于克服缺乏多重继承的问题。


0

这与劳伦斯·文纳姆(Lawrence Wenham)的回答是一致的,但是取决于您的用例,它可能是(也可能不是)一种改进-您不需要二传手。

public interface IPerson {
  int GetAge();
  string GetName();
}

public interface IGetPerson {
  IPerson GetPerson();
}

public static class IGetPersonAdditions {
  public static int GetAgeViaPerson(this IGetPerson getPerson) { // I prefer to have the "ViaPerson" in the name in case the object has another Age property.
    IPerson person = getPerson.GetPersion();
    return person.GetAge();
  }
  public static string GetNameViaPerson(this IGetPerson getPerson) {
    return getPerson.GetPerson().GetName();
  }
}

public class Person: IPerson, IGetPerson {
  private int Age {get;set;}
  private string Name {get;set;}
  public IPerson GetPerson() {
    return this;
  }
  public int GetAge() {  return Age; }
  public string GetName() { return Name; }
}

现在,任何知道如何招人的对象都可以实现IGetPerson,它将自动具有GetAgeViaPerson()和GetNameViaPerson()方法。从这一点来看,除了新的ivars,基本上所有的Person代码都输入IGetPerson,而不是IPerson,这两个都必须输入。并且在使用此类代码时,您不必担心IGetPerson对象本身是否实际上是IPerson。


0

现在这是可能的槽型partial类,它们每个都可以自己继承一个类,从而使最终对象继承所有基类。您可以在此处了解更多信息。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.