Herencia múltiple en C #


212

Dado que la herencia múltiple es mala (hace que la fuente sea más complicada) C # no proporciona dicho patrón directamente. Pero a veces sería útil tener esta habilidad.

Por ejemplo, puedo implementar el patrón de herencia múltiple faltante usando interfaces y tres clases como esa:

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

Cada vez que agrego un método a una de las interfaces, también necesito cambiar la clase FirstAndSecond .

¿Hay alguna manera de inyectar múltiples clases existentes en una nueva clase como es posible en C ++?

¿Tal vez hay una solución usando algún tipo de generación de código?

O puede verse así (sintaxis imaginaria de C #):

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

Para que no sea necesario actualizar la clase FirstAndSecond cuando modifique una de las interfaces.


EDITAR

Quizás sería mejor considerar un ejemplo práctico:

Tiene una clase existente (por ejemplo, un cliente TCP basado en texto basado en ITextTcpClient) que ya utiliza en diferentes ubicaciones dentro de su proyecto. Ahora siente la necesidad de crear un componente de su clase para que los desarrolladores de formularios de Windows puedan acceder fácilmente.

Hasta donde sé, actualmente tienes dos formas de hacer esto:

  1. Escriba una nueva clase que se herede de los componentes e implemente la interfaz de la clase TextTcpClient usando una instancia de la clase en sí como se muestra con FirstAndSecond.

  2. Escriba una nueva clase que herede de TextTcpClient y de alguna manera implemente IComponent (aún no lo he probado).

En ambos casos, debe trabajar por método y no por clase. Como sabe que necesitaremos todos los métodos de TextTcpClient y Component, sería la solución más fácil combinar esos dos en una sola clase.

Para evitar conflictos, esto se puede hacer mediante la generación de código, donde el resultado podría modificarse después, pero escribir esto a mano es un puro dolor de cabeza.


En la medida en que esto no es simplemente una herencia múltiple disfrazada, ¿cómo es menos complicado?
harpo

Pensando en los nuevos métodos de extensión en 3.5 y la forma en que funciona (generación de llamadas de miembros estáticos), esta podría ser una de las siguientes evoluciones del lenguaje .NET.
Larry

A veces me pregunto por qué la gente no solo hace ... clase A: clase B: clase C?
Chibueze Opata

@NazarMerza: el enlace ha cambiado. Ahora: el problema con la herencia múltiple .
Craig McQueen

99
No dejes que la propaganda te engañe. Su mismo ejemplo muestra que la herencia múltiple es útil y las interfaces son solo una solución para la falta de ella
Kemal Erdogan

Respuestas:


125

Dado que la herencia múltiple es mala (hace que la fuente sea más complicada) C # no proporciona dicho patrón directamente. Pero a veces sería útil tener esta habilidad.

C # y .net CLR no han implementado MI porque aún no han concluido cómo interactuaría entre C #, VB.net y los otros lenguajes, no porque "haría la fuente más compleja"

MI es un concepto útil, las preguntas sin respuesta son como: - "¿Qué haces cuando tienes múltiples clases base comunes en las diferentes superclases?

Perl es el único idioma con el que he trabajado donde MI funciona y funciona bien. Es posible que .Net lo presente algún día, pero aún no, el CLR ya admite MI pero, como he dicho, todavía no hay construcciones de lenguaje para eso.

Hasta entonces, estás atrapado con objetos proxy y múltiples interfaces en su lugar :(


39
El CLR no admite herencia de implementación múltiple, solo herencia de interfaz múltiple (que también es compatible con C #).
Jordão

44
@ Jordão: Para completar: es posible que los compiladores creen MI para sus tipos en el CLR. Tiene sus advertencias, no es compatible con CLS, por ejemplo. Para obtener más información, consulte este artículo (2004) blogs.msdn.com/b/csharpfaq/archive/2004/03/07/…
dvdvorle

2
@MrHappy: artículo muy interesante. De hecho, he investigado alguna forma de composición de rasgos para C #, eche un vistazo.
Jordão

10
@MandeepJanjua No reclamé tal cosa, dije 'bien podría presentarla'. El hecho es que el CLR estándar de ECMA proporciona la maquinaria IL para la herencia múltiple, solo que nada la utiliza por completo.
IanNorton

44
Para su información, la herencia múltiple no es mala y no complica el código. Solo pensé en mencionarlo.
Dmitri Nesteruk

214

Considere simplemente usar la composición en lugar de tratar de simular herencia múltiple. Puede usar Interfaces para definir qué clases componen la composición, por ejemplo: ISteerableimplica una propiedad de tipo SteeringWheel, IBrakableimplica una propiedad de tipo BrakePedal, etc.

Una vez que haya hecho eso, puede usar la función Métodos de extensión agregada a C # 3.0 para simplificar aún más los métodos de llamada en esas propiedades implícitas, por ejemplo:

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
Sin embargo, ese es el punto: una idea como esta facilitaría la composición.
Jon Skeet

99
Sí, pero hay casos de uso en los que realmente necesita los métodos como parte del objeto principal
David Pierre

99
Desafortunadamente, los métodos de extensión no pueden acceder a los datos de las variables miembro, por lo que debe exponerlos como públicos internos o (ug), aunque creo que la composición por contrato es la mejor manera de resolver la herencia múltiple.
cfeduke

44
Excelente respuesta! Conciso, fácil de entender, ilustración muy útil. ¡Gracias!
AJ.

3
Es posible que deseemos verificar si myCarha terminado de girar la dirección antes de llamar Stop. Podría volcarse si se Stopaplica mientras myCarestá a una velocidad excesiva. : D
Devraj Gadhavi

16

Creé un postcompilador C # que permite este tipo de cosas:

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

Puede ejecutar el compilador posterior como un evento posterior a la compilación de Visual Studio:

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

En el mismo ensamblaje lo usas así:

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

En otro ensamblaje lo usas así:

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

6

Podría tener una clase base abstracta que implemente tanto IFirst como ISecond, y luego heredar solo de esa base.


Esta es probablemente la mejor solución, pero no necesariamente la mejor idea: p
leppie

1
¿aún no tendría que editar la clase abstracta cuando agregue métodos a las interfaces?
Rik

Rik: ¿qué tan vago eres, cuando solo tienes que hacer esto una vez?
leppie

3
@leppie: "Cada vez que agrego un método a una de las interfaces, también necesito cambiar la clase FirstAndSecond". Esta parte de la pregunta original no se aborda en esta solución, ¿verdad?
Rik

2
Tendría que editar la clase abstracta, pero NO tiene que editar ninguna otra clase que dependa de ella. El dinero se detiene allí, en lugar de continuar en cascada a toda la colección de clases.
Joel Coehoorn

3

MI NO es malo, todos los que lo han usado (en serio) lo AMAN y ¡NO complica el código! Al menos no más que otras construcciones pueden complicar el código. El código incorrecto es un código incorrecto independientemente de si MI está en la imagen o no.

De todos modos, tengo una pequeña y agradable solución para herencia múltiple que quería compartir, es; http://ra-ajax.org/lsp-liskov-substitution-principle-to-be-or-not-to-be.blog o puede seguir el enlace en mi sig ... :)


¿Es posible tener herencia múltiple y al mismo tiempo tener subidas y bajadas preservar la identidad? Las soluciones que conozco para los problemas de la herencia múltiple giran en torno a tener modelos que no preservan la identidad (si myFooes de tipo Foo, que hereda de Mooy Goo, ambos heredan de Boo, entonces (Boo)(Moo)myFooy (Boo)(Goo)myFoono serían equivalentes). ¿Conoces algún enfoque para preservar la identidad?
supercat

1
Puede usar web.archive.org o similar para seguir el enlace, pero resulta ser una discusión más detallada de exactamente la solución ofrecida en la pregunta original aquí.
MikeBeaton

2

En mi propia implementación, descubrí que el uso de clases / interfaces para MI, aunque "buena forma", tendía a ser una complicación enorme, ya que necesita configurar toda esa herencia múltiple para solo unas pocas llamadas de función necesarias, y en mi caso, necesitaba hacerse literalmente docenas de veces de forma redundante.

En cambio, era más fácil simplemente hacer "funciones que llaman funciones que llaman funciones" estáticas en diferentes variedades modulares como una especie de reemplazo de OOP. La solución en la que estaba trabajando era el "sistema de hechizos" para un juego de rol en el que los efectos debían mezclar y combinar llamadas de funciones para dar una variedad extrema de hechizos sin reescribir el código, como parece indicar el ejemplo.

La mayoría de las funciones ahora pueden ser estáticas porque no necesito necesariamente una instancia para la lógica ortográfica, mientras que la herencia de clase ni siquiera puede usar palabras clave virtuales o abstractas mientras está estática. Las interfaces no pueden usarlos en absoluto.

La codificación parece mucho más rápida y limpia de esta manera en mi opinión. Si solo está haciendo funciones y no necesita propiedades heredadas , use funciones.


2

Con C # 8 ahora prácticamente tiene herencia múltiple a través de la implementación predeterminada de los miembros de la interfaz:

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
}

44
Sí, pero tenga en cuenta que, en lo anterior, no podrá hacerlo new ConsoleLogger().Log(someEception): simplemente no funcionará, tendría que convertir explícitamente su objeto a ILoggerpara usar el método de interfaz predeterminado. Por lo tanto, su utilidad es algo limitada.
Dmitri Nesteruk

1

Si puede vivir con la restricción de que los métodos de IFirst e ISecond solo deben interactuar con el contrato de IFirst e ISecond (como en su ejemplo) ... puede hacer lo que le pida con los métodos de extensión. En la práctica, este rara vez es el 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();
}

Entonces, la idea básica es que defina la implementación requerida en las interfaces ... este material requerido debe admitir la implementación flexible en los métodos de extensión. Cada vez que necesite "agregar métodos a la interfaz", agregue un método de extensión.


1

Sí, usar la interfaz es una molestia porque cada vez que agregamos un método en la clase tenemos que agregar la firma en la interfaz. Además, ¿qué pasa si ya tenemos una clase con un montón de métodos pero no tenemos una interfaz? tenemos que crear manualmente la interfaz para todas las clases de las que queremos heredar. Y lo peor es que tenemos que implementar todos los métodos en las interfaces en la clase secundaria para que la clase secundaria herede de la interfaz múltiple.

Siguiendo el patrón de diseño de Fachada, podemos simular la herencia de múltiples clases usando accesores . Declare las clases como propiedades con {get; set;} dentro de la clase que necesita heredar y todas las propiedades y métodos públicos son de esa clase, y en el constructor de la clase secundaria instancia las clases primarias.

Por ejemplo:

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


 }

con esta estructura, la clase Child tendrá acceso a todos los métodos y propiedades de la clase Father and Mother, simulando herencia múltiple, heredando una instancia de las clases padre. No es lo mismo pero es práctico.


2
No estoy de acuerdo con el primer párrafo. Solo agrega las firmas de los métodos que desea en CADA clase a la interfaz. Pero puede agregar tantos métodos adicionales a cualquier clase como desee. Además, hay un clic derecho, interfaz de extracción que simplifica el trabajo de extracción de interfaces. Finalmente, su ejemplo no es de ninguna manera herencia (múltiple o no), sin embargo, es un gran ejemplo de composición. Por desgracia, tendría más mérito si hubiera utilizado interfaces para demostrar también DI / IOC utilizando la inyección de constructor / propiedad. Si bien no voy a votar, no creo que sea una buena respuesta, lo siento.
Francis Rodgers

1
Mirando hacia atrás en este hilo un año después, estoy de acuerdo en que puede agregar tantos métodos como desee en la clase sin agregar firma en la interfaz, sin embargo, esto podría hacer que la interfaz sea incompleta. Además, no pude encontrar la interfaz de extracción con el botón derecho en mi IDE, tal vez me faltaba algo. Pero, mi mayor preocupación es que, cuando especifica una firma en la interfaz, las clases heredadas deben implementar esa firma. Creo que este es un trabajo doble y podría conducir a la duplicación de códigos.
Yogi

INTERFAZ DE EXTRACTO: haga clic derecho sobre la firma de la clase, luego extraiga la interfaz ... en VS2015 mismo proceso, excepto que debe hacer clic con el botón derecho, luego elegir Quick Actions and Refactorings...esto es algo que debe saber, le ahorrará mucho tiempo
Chef_Code

1

Todos parecemos estar avanzando por la ruta de la interfaz con esto, pero la otra posibilidad obvia, aquí, es hacer lo que se supone que debe hacer OOP, y construir su árbol de herencia ... (¿no es esto lo que el diseño de clase es todo? ¿acerca de?)

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

Esta estructura proporciona bloques de código reutilizables y, seguramente, ¿cómo se debe escribir el código OOP?

Si este enfoque en particular no se ajusta a la factura, simplemente creamos nuevas clases basadas en los objetos requeridos ...

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

La herencia múltiple es una de esas cosas que generalmente causa más problemas de los que resuelve. En C ++, se ajusta al patrón de darle suficiente cuerda para colgarse, pero Java y C # han optado por la ruta más segura de no darle la opción. El mayor problema es qué hacer si hereda varias clases que tienen un método con la misma firma que el heredado no implementa. ¿Qué método de clase debería elegir? ¿O no debería eso compilar? Generalmente hay otra forma de implementar la mayoría de las cosas que no depende de la herencia múltiple.


8
No juzgues MI por C ++, es como juzgar OOP por PHP o automóviles por Pintos. Ese problema es fácil de resolver: en Eiffel, cuando heredas de una clase, también tienes que especificar qué métodos quieres heredar y puedes cambiarles el nombre. No hay ambigüedades allí y tampoco sorpresas.
Jörg W Mittag

2
@mP: no, Eiffel proporciona una verdadera herencia de implementación múltiple. Cambiar el nombre no significa perder la cadena de herencia, ni perderá la capacidad de conversión de las clases.
Abel

0

Si X hereda de Y, eso tiene dos efectos algo ortogonales:

  1. Y proporcionará la funcionalidad predeterminada para X, por lo que el código para X solo debe incluir cosas que sean diferentes de Y.
  2. Casi en cualquier lugar se esperaría una Y, se puede usar una X en su lugar.

Aunque la herencia proporciona ambas características, no es difícil imaginar circunstancias en las que cualquiera podría ser útil sin la otra. Ningún lenguaje .net que conozco tiene una forma directa de implementar el primero sin el segundo, aunque uno podría obtener dicha funcionalidad definiendo una clase base que nunca se usa directamente, y teniendo una o más clases que hereden directamente de ella sin agregar nada nuevo (tales clases podrían compartir todo su código, pero no serían sustituibles entre sí). Sin embargo, cualquier lenguaje compatible con CLR permitirá el uso de interfaces que proporcionan la segunda característica de las interfaces (sustituibilidad) sin la primera (reutilización de miembros).


0

Sé que lo sé a pesar de que no está permitido, etc., en algún momento realmente lo necesitas para aquellos:

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

como en mi caso, quería hacer esta clase b: Formulario (sí, windows.forms) clase c: b {}

porque la mitad de la función era idéntica y con la interfaz debes reescribirlas todas


1
Su ejemplo no representa la herencia múltiple, por lo que ¿Qué problema que trata de resolver? Se mostraría un verdadero ejemplo de herencia múltiple class a : b, c(implementando los agujeros contractuales necesarios). ¿Quizás tus ejemplos están un poco simplificados?
M.Babcock

0

Dado que la cuestión de la herencia múltiple (MI) aparece de vez en cuando, me gustaría agregar un enfoque que aborde algunos problemas con el patrón de composición.

Construyo sobre el IFirst, ISecond, First, Second, FirstAndSecondenfoque, tal como se presentó en la pregunta. Reduzco el código de muestra a IFirst, ya que el patrón permanece igual independientemente del número de interfaces / clases base de MI.

Supongamos que con MI Firsty Secondambos derivarían de la misma clase base BaseClass, usando solo elementos de interfaz pública deBaseClass

Esto se puede expresar agregando una referencia de contenedor a BaseClassla implementación Firsty Second:

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

Las cosas se vuelven más complicadas, cuando BaseClassse hace referencia a elementos de interfaz protegidos o cuando Firsty Secondserían clases abstractas en MI, lo que requiere que sus subclases implementen algunas partes abstractas.

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 # permite que las clases anidadas accedan a elementos protegidos / privados de sus clases que contienen, por lo que esto puede usarse para vincular los bits abstractos de la Firstimplementación.

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

Hay bastante repetitivo, pero si la implementación real de FirstMethod y SecondMethod es suficientemente compleja y la cantidad de métodos privados / protegidos a los que se accede es moderada, entonces este patrón puede ayudar a superar la falta de herencia múltiple.


0

Esto está en la línea de la respuesta de Lawrence Wenham, pero dependiendo de su caso de uso, puede o no ser una mejora: no necesita los preparadores.

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

Ahora, cualquier objeto que sepa cómo obtener una persona puede implementar IGetPerson, y automáticamente tendrá los métodos GetAgeViaPerson () y GetNameViaPerson (). Desde este punto, básicamente todo el código de persona va a IGetPerson, no a IPerson, excepto los nuevos ivars, que deben ir a ambos. Y al usar dicho código, no tiene que preocuparse por si su objeto IGetPerson es en realidad un IPerson.


0

Esto ahora es posible a través de partialclases, cada una de ellas puede heredar una clase por sí misma, haciendo que el objeto final herede todas las clases base. Puede obtener más información al respecto aquí .

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.