Pregunta

Tengo algunos problemas de herencia porque tengo un grupo de clases abstractas interrelacionadas que deben anularse todas juntas para crear una implementación de cliente.Lo ideal sería hacer algo como lo siguiente:

abstract class Animal
{
  public Leg GetLeg() {...}
}

abstract class Leg { }

class Dog : Animal
{
  public override DogLeg Leg() {...}
}

class DogLeg : Leg { }

Esto permitiría que cualquiera que use la clase Perro obtenga automáticamente DogLegs y cualquiera que use la clase Animal obtenga Legs.El problema es que la función anulada debe tener el mismo tipo que la clase base, por lo que no se compilará.Sin embargo, no veo por qué no debería hacerlo, ya que DogLeg se puede convertir implícitamente a Leg.Sé que hay muchas formas de solucionar esto, pero tengo más curiosidad por saber por qué esto no es posible/implementado en C#.

EDITAR:Modifiqué esto un poco, ya que en realidad estoy usando propiedades en lugar de funciones en mi código.

EDITAR:Lo cambié nuevamente a funciones, porque la respuesta solo se aplica a esa situación (covarianza en el parámetro de valor de la función establecida de una propiedad no debería trabajar).¡Perdón por las fluctuaciones!Me doy cuenta de que hace que muchas de las respuestas parezcan irrelevantes.

¿Fue útil?

Solución

La respuesta corta es que GetLeg es invariante en su tipo de retorno.La respuesta larga se puede encontrar aquí: Covarianza y contravarianza

Me gustaría agregar que, si bien la herencia suele ser la primera herramienta de abstracción que la mayoría de los desarrolladores sacan de su caja de herramientas, casi siempre es posible utilizar la composición en su lugar.La composición supone un poco más de trabajo para el desarrollador de API, pero hace que la API sea más útil para sus consumidores.

Otros consejos

Claramente, necesitarás un elenco si estás operando en un dogleg roto.

El perro debe devolver una pierna, no una pierna de perro como tipo de devolución.La clase real puede ser DogLeg, pero el punto es desacoplar para que el usuario de Dog no tenga que saber acerca de DogLegs, solo necesita saber acerca de Legs.

Cambiar:

class Dog : Animal
{
  public override DogLeg GetLeg() {...}
}

a:

class Dog : Animal
{
  public override Leg GetLeg() {...}
}

No hagas esto:

 if(a instanceof Dog){
       DogLeg dl = (DogLeg)a.GetLeg();

anula el propósito de programar al tipo abstracto.

La razón para ocultar DogLeg es porque la función GetLeg en la clase abstracta devuelve un Abstract Leg.Si está anulando GetLeg, debe devolver un tramo.Ese es el punto de tener un método en una clase abstracta.Para propagar ese método a sus hijos.Si desea que los usuarios de Dog sepan acerca de DogLegs, cree un método llamado GetDogLeg y devuelva un DogLeg.

Si PODRÍA hacer lo que quiere el autor de la pregunta, entonces cada usuario de Animal necesitaría saber sobre TODOS los animales.

Es un deseo perfectamente válido que la firma de un método anulado tenga un tipo de retorno que sea un subtipo del tipo de retorno en el método anulado (Uf).Después de todo, son compatibles con el tipo de tiempo de ejecución.

Pero C# aún no admite "tipos de retorno covariantes" en métodos anulados (a diferencia de C++ [1998] y Java [2004]).

Tendrá que trabajar y arreglárselas en el futuro previsible, como afirmó Eric Lippert en su blog[19 de junio de 2008]:

Ese tipo de variación se llama "covarianza de tipo de retorno".

No tenemos planes de implementar ese tipo de variación en C#.

abstract class Animal
{
  public virtual Leg GetLeg ()
}

abstract class Leg { }

class Dog : Animal
{
  public override Leg GetLeg () { return new DogLeg(); }
}

class DogLeg : Leg { void Hump(); }

Hágalo así, luego podrá aprovechar la abstracción en su cliente:

Leg myleg = myDog.GetLeg();

Luego, si es necesario, puedes transmitirlo:

if (myleg is DogLeg) { ((DogLeg)myLeg).Hump()); }

Totalmente ideado, pero el punto es que puedes hacer esto:

foreach (Animal a in animals)
{
   a.GetLeg().SomeMethodThatIsOnAllLegs();
}

Sin dejar de conservar la capacidad de tener un método Hump especial en Doglegs.

Puede usar genéricos e interfaces para implementar eso en C#:

abstract class Leg { }

interface IAnimal { Leg GetLeg(); }

abstract class Animal<TLeg> : IAnimal where TLeg : Leg
 { public abstract TLeg GetLeg();
   Leg IAnimal.GetLeg() { return this.GetLeg(); }
 }

class Dog : Animal<Dog.DogLeg>
 { public class DogLeg : Leg { }
   public override DogLeg GetLeg() { return new DogLeg();}
 } 

GetLeg() debe devolver Leg para que sea una anulación.Sin embargo, su clase Dog aún puede devolver objetos DogLeg ya que son una clase secundaria de Leg.Luego, los clientes pueden moldearlos y operarlos como si fueran doglegs.

public class ClientObj{
    public void doStuff(){
    Animal a=getAnimal();
    if(a is Dog){
       DogLeg dl = (DogLeg)a.GetLeg();
    }
  }
}

El concepto que le está causando problemas se describe en http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)

No es que sea de mucha utilidad, pero tal vez sea interesante notar que Java admite retornos covariantes, por lo que esto funcionaría exactamente como esperaba.Excepto obviamente que Java no tiene propiedades;)

Quizás sea más fácil ver el problema con un ejemplo:

Animal dog = new Dog();
dog.SetLeg(new CatLeg());

Eso debería compilarse si eres un perro compilado, pero probablemente no queramos un mutante así.

Un problema relacionado es ¿Debería Dog[] ser un Animal[] o IList<Dog> un IList<Animal>?

C# tiene implementaciones de interfaz explícitas para abordar precisamente este problema:

abstract class Leg { }
class DogLeg : Leg { }

interface IAnimal
{
    Leg GetLeg();
}

class Dog : IAnimal
{
    public override DogLeg GetLeg() { /* */ }

    Leg IAnimal.GetLeg() { return GetLeg(); }
}

Si tiene un Perro a través de una referencia de tipo Perro, llamar a GetLeg() devolverá un DogLeg.Si tiene el mismo objeto, pero la referencia es de tipo IAnimal, devolverá una Pierna.

Bien, entiendo que puedo simplemente lanzar, pero eso significa que el cliente debe saber que los perros tienen DogLegs.Lo que me pregunto es si existen razones técnicas por las que esto no es posible, dado que existe una conversión implícita.

@Brian Leahy Obviamente si solo está operando como una pierna, no hay necesidad ni razón para lanzar.Pero si hay algún comportamiento específico de DogLeg o Dog, a veces hay razones por las que el yeso es necesario.

También puede devolver la interfaz ILeg que implementan tanto Leg como DogLeg.

Lo importante que debe recordar es que puede usar un tipo derivado cada vez que use el tipo base (puede pasar Perro a cualquier método/propiedad/campo/variable que espere Animal)

Tomemos esta función:

public void AddLeg(Animal a)
{
   a.Leg = new Leg();
}

Una función perfectamente válida, ahora llamemos a la función así:

AddLeg(new Dog());

Si la propiedad Dog.Leg no es del tipo Leg, la función AddLeg de repente contiene un error y no se puede compilar.

@Lucas

Creo que quizás hayas malinterpretado la herencia.Dog.GetLeg() devolverá un objeto DogLeg.

public class Dog{
    public Leg GetLeg(){
         DogLeg dl = new DogLeg(super.GetLeg());
         //set dogleg specific properties
    }
}


    Animal a = getDog();
    Leg l = a.GetLeg();
    l.kick();

el método real llamado será Dog.GetLeg();y DogLeg.Kick() (supongo que existe un método Leg.kick()) para eso, el tipo de retorno declarado DogLeg es innecesario, porque eso es lo que se devolvió, incluso si el tipo de retorno para Dog.GetLeg() es Pierna.

Puede lograr lo que desea utilizando un genérico con una restricción adecuada, como la siguiente:

abstract class Animal<LegType> where LegType : Leg
{
    public abstract LegType GetLeg();
}

abstract class Leg { }

class Dog : Animal<DogLeg>
{
    public override DogLeg GetLeg()
    {
        return new DogLeg();
    }
}

class DogLeg : Leg { }
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top