Herança múltipla em C #

212

Como a herança múltipla é ruim (torna a fonte mais complicada), o C # não fornece esse padrão diretamente. Mas, às vezes, seria útil ter essa capacidade.

Por exemplo, eu sou capaz de implementar o padrão de herança múltipla ausente usando interfaces e três classes como essa:

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(); }
}

Toda vez que adiciono um método a uma das interfaces, também preciso alterar a classe FirstAndSecond .

Existe uma maneira de injetar várias classes existentes em uma nova classe, como é possível em C ++?

Talvez haja uma solução usando algum tipo de geração de código?

Ou pode ficar assim (sintaxe c # imaginária):

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

Para que não seja necessário atualizar a classe FirstAndSecond quando eu modificar uma das interfaces.


EDITAR

Talvez seja melhor considerar um exemplo prático:

Você tem uma classe existente (por exemplo, um cliente TCP baseado em texto, com base no ITextTcpClient), que você já usa em diferentes locais dentro do seu projeto. Agora você sente a necessidade de criar um componente da sua classe para ser facilmente acessível aos desenvolvedores de formulários do Windows.

Até onde eu sei, atualmente você tem duas maneiras de fazer isso:

  1. Escreva uma nova classe que seja herdada dos componentes e implemente a interface da classe TextTcpClient usando uma instância da própria classe, como mostrado em FirstAndSecond.

  2. Escreva uma nova classe que herda de TextTcpClient e de alguma forma implemente IComponent (na verdade, ainda não tentei isso).

Nos dois casos, você precisa trabalhar por método e não por classe. Como você sabe que precisaremos de todos os métodos de TextTcpClient e Component, seria a solução mais fácil apenas combinar os dois em uma classe.

Para evitar conflitos, isso pode ser feito por geração de código, onde o resultado pode ser alterado posteriormente, mas digitar manualmente é uma pura dor de cabeça.

Martin
fonte
Na medida em que isso não é simplesmente herança múltipla disfarçada, como é menos complicado?
harpo 07/10/08
Pensando nos novos métodos de extensão da 3.5 e na maneira como ela funciona (geração de chamada de membro estático), essa pode ser uma das próximas evoluções da linguagem .NET.
Larry
Às vezes me pergunto por que as pessoas não fazem apenas ... classe A: classe B: classe C?
Chibueze Opata
@NazarMerza: O link mudou. Agora: o problema com múltiplas heranças .
Craig McQueen
9
Não deixe a propaganda enganar você. Seu exemplo muito mostra que a herança múltipla é útil e interfaces é apenas uma solução para a falta dela
Kemal Erdogan

Respostas:

125

Como a herança múltipla é ruim (torna a fonte mais complicada), o C # não fornece esse padrão diretamente. Mas, às vezes, seria útil ter essa capacidade.

O C # e o .net CLR não implementaram o MI porque ainda não concluíram como ele interoperaria entre o C #, o VB.net e os outros idiomas, não porque "tornaria a fonte mais complexa"

O MI é um conceito útil, e as perguntas não respondidas são: - "O que você faz quando tem várias classes de base comuns nas diferentes superclasses?

Perl é a única linguagem com a qual trabalhei onde o MI funciona e funciona bem. A .net pode muito bem apresentá-lo um dia, mas ainda não, o CLR já suporta MI, mas, como eu disse, ainda não há construções de linguagem para ele.

Até então, você fica preso com objetos Proxy e várias interfaces :(

IanNorton
fonte
39
O CLR não suporta herança múltipla de implementação, apenas herança múltipla de interface (que também é suportada em C #).
Jordão
4
@ Jordão: por questões de integridade: é possível que os compiladores criem MI para seus tipos no CLR. Tem suas ressalvas, não é compatível com CLS, por exemplo. Para obter mais informações, consulte este artigo (2004) blogs.msdn.com/b/csharpfaq/archive/2004/03/07/…
dvdvorle
2
@ MrHappy: Artigo muito interessante. Na verdade, investiguei uma maneira de composição de características para C #, dê uma olhada.
Jordão
10
@MandeepJanjua Eu não reivindiquei nada disso, eu disse 'pode muito bem apresentá-lo'. O fato é que o CLR padrão da ECMA fornece o mecanismo de IL para herança múltipla, apenas que nada o utiliza totalmente.
21912 IanNorton
4
A herança múltipla da EFY não é ruim e não torna o código tão complicado. Só pensei em mencionar.
Dmitri Nesteruk
214

Considere apenas usar a composição em vez de tentar simular herança múltipla. Você pode usar interfaces para definir quais classes compõem a composição, por exemplo: ISteerableimplica uma propriedade do tipo SteeringWheel, IBrakableimplica uma propriedade do tipoBrakePedal , etc.

Depois de fazer isso, você poderá usar o recurso Extension Methods adicionado ao C # 3.0 para simplificar ainda mais os métodos de chamada nessas propriedades implícitas, por exemplo:

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();
    }
}
Chris Wenham
fonte
13
Esse é o ponto - uma ideia como essa facilitaria a composição.
Jon Skeet
9
Sim, mas há casos de uso onde você realmente precisa os métodos como parte do objeto principal
David Pierre
9
Infelizmente, os dados variáveis ​​dos membros não estão acessíveis nos métodos de extensão, então você deve expô-los como público interno ou externo, embora eu ache que a composição por contrato é a melhor maneira de resolver a herança múltipla.
cfeduke 10/10/08
4
Excelente resposta! Ilustração concisa, fácil de entender e muito útil. Obrigado!
AJ.
3
Convém verificar se a myCardireção terminou antes de ligar Stop. Pode rolar se Stopaplicado enquanto myCarestiver em velocidade excessiva. : D
Devraj Gadhavi
16

Eu criei um pós-compilador em C # que permite esse tipo de coisa:

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> { }

Você pode executar o pós-compilador como um evento de pós-compilação do Visual Studio:

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

Na mesma montagem, você a usa assim:

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

Em outra montagem, você a usa assim:

var fas = new FirstAndSecond();
fas.FirstMethod();
fas.SecondMethod();
Jordão
fonte
6

Você poderia ter uma classe base abstrata que implementa IFirst e ISecond e, em seguida, herdar apenas dessa base.

Joel Coehoorn
fonte
Esta é provavelmente a melhor solução, mas não necessariamente a melhor idéia: p
leppie
1
você ainda não precisaria editar a classe abstrata ao adicionar métodos às interações?
Rik
Rik: quão preguiçoso você é, quando você só precisa fazer isso uma vez?
leppie
3
@leppie - "Toda vez que adiciono um método a uma das interfaces, também preciso alterar a classe FirstAndSecond." Esta parte da pergunta original não é abordada por esta solução, é?
Rik
2
Você precisaria editar a classe abstrata, mas NÃO precisará editar nenhuma outra classe que dependa dela. O dinheiro pára por aí, em vez de continuar em cascata para toda a coleção de classes.
Joel Coehoorn
3

MI NÃO é ruim, todo mundo que (seriamente) o usou AMA e NÃO complica o código! Pelo menos não mais do que outras construções podem complicar o código. Código incorreto é código incorreto, independentemente de o MI estar ou não na imagem.

De qualquer forma, eu tenho uma ótima solução para a Herança Múltipla que eu gostaria de compartilhar. http://ra-ajax.org/lsp-liskov-substitution-principle-to-be-or-not-to-be.blog ou você pode seguir o link no meu sig ... :)

Thomas Hansen
fonte
É possível ter herança múltipla e, ao mesmo tempo, manter upcasts e downcasts preservando a identidade? As soluções que conheço para os problemas de herança múltipla giram em torno de ter elencos que não preservam a identidade (se myFooé do tipo Foo, que herda de Mooe Goo, os quais herdam de Boo, então (Boo)(Moo)myFooe (Boo)(Goo)myFoonão seriam equivalentes). Você conhece alguma abordagem de preservação de identidade?
supercat
1
Você pode usar web.archive.org ou similar para seguir o link, mas acaba sendo uma discussão mais detalhada sobre exatamente a solução oferecida na pergunta original aqui.
MikeBeaton 27/09/19
2

Em minha própria implementação, descobri que o uso de classes / interfaces para MI, embora "em boa forma", tendia a ser uma complicação excessiva, pois você precisa configurar toda essa herança múltipla para apenas algumas chamadas de função necessárias e, no meu caso, precisava ser feito literalmente dezenas de vezes de forma redundante.

Em vez disso, era mais fácil simplesmente criar "funções que chamam funções que chamam funções" estáticas em diferentes variedades modulares como uma espécie de substituição de POO. A solução que eu estava trabalhando em foi o "sistema de feitiço" para um RPG onde os efeitos precisa fortemente mix-and-match função de chamada para dar uma extrema variedade de magias, sem código de escrita re, muito parecido com o exemplo parece indicar.

A maioria das funções agora pode ser estática, porque eu não preciso necessariamente de uma instância para lógica ortográfica, enquanto a herança de classe nem sequer pode usar palavras-chave virtuais ou abstratas enquanto estática. As interfaces não podem usá-las.

A codificação parece muito mais rápida e limpa dessa maneira, na IMO. Se você está apenas executando funções e não precisa de propriedades herdadas , use funções.

hydrix
fonte
2

Com o C # 8 agora você praticamente tem herança múltipla via implementação padrão dos membros da interface:

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
}
Ludmil Tinkov
fonte
4
Sim, mas observe que, acima, você não seria capaz de fazê new ConsoleLogger().Log(someEception)-lo - simplesmente não funcionaria; seria necessário converter explicitamente seu objeto em um ILoggerpara usar o método de interface padrão. Portanto, sua utilidade é um pouco limitada.
Dmitri Nesteruk
1

Se você pode conviver com a restrição de que os métodos do IFirst e do ISecond devem interagir apenas com o contrato do IFirst e do ISecond (como no seu exemplo) ... você pode fazer o que solicita com os métodos de extensão. Na prática, isso raramente é o caso.

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();
}

Portanto, a idéia básica é que você defina a implementação necessária nas interfaces ... esse material necessário deve suportar a implementação flexível nos métodos de extensão. Sempre que você precisar "adicionar métodos à interface", adicione um método de extensão.

Amy B
fonte
1

Sim, usar Interface é um aborrecimento, porque sempre que adicionamos um método na classe, precisamos adicionar a assinatura na interface. Além disso, e se já tivermos uma classe com vários métodos, mas sem interface? precisamos criar manualmente a interface para todas as classes das quais queremos herdar. E o pior é que temos que implementar todos os métodos nas interfaces na classe filho para que a classe filho seja herdada da interface múltipla.

Seguindo o padrão de design do Facade, podemos simular a herança de várias classes usando acessadores . Declare as classes como propriedades com {get; set;} dentro da classe que precisa herdar e todas as propriedades e métodos públicos são dessa classe e, no construtor da classe filho, instanciam as classes pai.

Por exemplo:

 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();
         }
     }


 }

com essa estrutura, a classe Child terá acesso a todos os métodos e propriedades da classe Pai e Mãe, simulando herança múltipla, herdando uma instância das classes pai. Não é exatamente o mesmo, mas é prático.

Yogi
fonte
2
Não concordo com o primeiro parágrafo. Você só adiciona as assinaturas dos métodos que deseja em TODAS as classes à interface. Mas você pode adicionar quantos métodos adicionais quiser a qualquer classe. Além disso, há um clique com o botão direito do mouse, interface de extração, que simplifica o trabalho de extrair interfaces. Finalmente, seu exemplo não é de forma alguma herança (múltipla ou não), mas é um ótimo exemplo de composição. Infelizmente, teria mais mérito se você tivesse usado interfaces para também demonstrar DI / IOC usando injeção de construtor / propriedade. Enquanto não votar, não acho que seja uma boa resposta.
Francis Rodgers
1
Olhando para esse tópico um ano depois, concordo que você pode adicionar quantos métodos desejar na classe sem adicionar assinatura na interface; no entanto, isso tornaria a interface incompleta. Além disso, não consegui encontrar a interface de extração com o botão direito do mouse no meu IDE, talvez estivesse faltando alguma coisa. Mas, minha maior preocupação é que, quando você especifica uma assinatura na interface, as classes herdadas devem implementar essa assinatura. Eu acho que isso é um trabalho duplo e pode levar à duplicação de códigos.
Yogi
EXTRATO DE INTERFACE: clique direito sobre a assinatura de classe, em seguida, extrair interface de ... em VS2015 mesmo processo , exceto você deve clicar direita, em seguida, escolher Quick Actions and Refactorings...este é um deve saber, ela vai lhe poupar muito tempo
Chef_Code
1

Todos nós parecemos estar seguindo o caminho da interface com isso, mas a outra possibilidade óbvia, aqui, é fazer o que o OOP deve fazer e construir sua árvore de herança ... (não é isso que design de classe é tudo sobre?)

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; }
}

Essa estrutura fornece blocos de código reutilizáveis ​​e, certamente, é como o código OOP deve ser escrito?

Se essa abordagem em particular não se encaixar perfeitamente, simplesmente criamos novas classes com base nos objetos necessários ...

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; }
}
Paulo
fonte
0

A herança múltipla é uma daquelas coisas que geralmente causa mais problemas do que resolve. No C ++, ele se encaixa no padrão de fornecer corda suficiente para você se enforcar, mas Java e C # optaram por seguir a rota mais segura de não oferecer a opção. O maior problema é o que fazer se você herdar várias classes que possuem um método com a mesma assinatura que o herdado não implementa. Qual método de classe deve escolher? Ou isso não deve compilar? Geralmente, existe outra maneira de implementar a maioria das coisas que não depende de herança múltipla.

abordagem
fonte
8
Por favor, não julgue MI por C ++, é como julgar OOP por PHP ou automóveis por Pintos. Esse problema é facilmente solucionável: em Eiffel, quando você herda de uma classe, também precisa especificar quais métodos deseja herdar e pode renomeá-los. Não há ambigüidades e nem surpresas.
Jörg W Mittag
2
@mP: não, Eiffel fornece herança de implementação múltipla verdadeira. Renomear não significa perder a cadeia de herança, nem perderá a possibilidade de castidade das classes.
Abel
0

Se X herda de Y, isso tem dois efeitos um tanto ortogonais:

  1. Y fornecerá a funcionalidade padrão para X, portanto, o código para X deve incluir apenas itens diferentes de Y.
  2. Quase em qualquer lugar que um Y fosse esperado, um X pode ser usado.

Embora a herança preveja os dois recursos, não é difícil imaginar circunstâncias em que uma delas possa ser útil sem a outra. Nenhuma linguagem .net que eu conheça tem uma maneira direta de implementar a primeira sem a segunda, embora se possa obter essa funcionalidade definindo uma classe base que nunca é usada diretamente e possuindo uma ou mais classes que herdam diretamente dela sem adicionar nada novo (essas classes poderiam compartilhar todo o código, mas não seriam substituíveis uma pela outra). Qualquer linguagem compatível com CLR, no entanto, permitirá o uso de interfaces que fornecem o segundo recurso de interfaces (substituibilidade) sem o primeiro (reutilização de membro).

supercat
fonte
0

Eu sei que sei que, embora não seja permitido e assim por diante, às vezes você realmente precisa disso para aqueles:

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

como no meu caso, eu queria fazer essa classe b: Form (sim, windows.forms) classe c: b {}

porque metade da função era idêntica e com a interface você deve reescrevê-las todas

ariel
fonte
1
Seu exemplo não descreve herança múltipla, então qual problema você está tentando resolver? Um exemplo de herança múltipla verdadeira seria exibido class a : b, c(implementando quaisquer buracos de contrato necessários). Talvez seus exemplos sejam simplificados demais?
precisa saber é o seguinte
0

Como a questão da herança múltipla (IM) surge de tempos em tempos, eu gostaria de adicionar uma abordagem que resolva alguns problemas com o padrão de composição.

Eu construir sobre o IFirst, ISecond, First, Second, FirstAndSecondabordagem, como foi apresentado na pergunta. Reduzo o código de exemplo para IFirst, pois o padrão permanece o mesmo, independentemente do número de interfaces / classes básicas do MI.

Vamos supor que, com MI Firste Secondque ambos derivam da mesma classe base BaseClass, utilizando elementos de interface única públicas deBaseClass

Isso pode ser expresso, adicionando uma referência de contêiner BaseClassno Firste na Secondimplementação:

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

As coisas se tornam mais complicadas, quando elementos de interface protegidos BaseClasssão referenciados ou quando Firste Secondseriam classes abstratas no MI, exigindo que suas subclasses implementem algumas partes abstratas.

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
}

O C # permite que classes aninhadas acessem elementos protegidos / privados de suas classes que os contêm, portanto, isso pode ser usado para vincular os bits abstratos da Firstimplementação.

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"); }
}

Há bastante clichê envolvido, mas se a implementação real do FirstMethod e SecondMethod for suficientemente complexa e a quantidade de métodos privados / protegidos acessados ​​for moderada, esse padrão poderá ajudar a superar a falta de herança múltipla.

grek40
fonte
0

Isso segue as linhas da resposta de Lawrence Wenham, mas, dependendo do seu caso de uso, pode ou não ser uma melhoria - você não precisa dos levantadores.

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; }
}

Agora, qualquer objeto que saiba como obter uma pessoa pode implementar IGetPerson e terá automaticamente os métodos GetAgeViaPerson () e GetNameViaPerson (). A partir deste ponto, basicamente todo o código Person entra no IGetPerson, não no IPerson, exceto os novos ivars, que precisam entrar nos dois. E ao usar esse código, você não precisa se preocupar se o objeto IGetPerson é ou não um IPerson.

William Jockusch
fonte
0

Agora isso é possível através de partialclasses, cada uma delas pode herdar uma classe por conta própria, fazendo com que o objeto final herde todas as classes base. Você pode aprender mais sobre isso aqui .

Nekuś
fonte