C #: invalidar tipos de retorno

81

¿Hay alguna forma de anular los tipos de retorno en C #? Si es así, ¿cómo, y si no, por qué y cuál es la forma recomendada de hacerlo?

Mi caso es que tengo una interfaz con una clase base abstracta y sus descendientes. Me gustaría hacer esto (está bien, no realmente, ¡pero como ejemplo!):

public interface Animal
{
   Poo Excrement { get; }
}

public class AnimalBase
{
   public virtual Poo Excrement { get { return new Poo(); } }
}

public class Dog
{
  // No override, just return normal poo like normal animal
}

public class Cat
{
  public override RadioactivePoo Excrement { get { return new RadioActivePoo(); } }
}

RadioactivePoopor supuesto hereda de Poo.

Mi razón para querer esto es para que los que utilizan Catlos objetos podría utilizar la Excrementpropiedad sin tener que emitir el Pooal RadioactivePoomismo tiempo, por ejemplo, el Cattodavía podría ser parte de una Animallista en la que los usuarios pueden no ser necesariamente conscientes o la atención acerca de su caca radiactivo. Espero que tenga sentido ...

Por lo que puedo ver, el compilador no permite esto al menos. Entonces supongo que es imposible. Pero, ¿qué recomendaría como solución a esto?

Svish
fuente
2
¿Qué pasa con los genéricos? ¿No ayudarían?
Arnis Lapsa
3
¿No debería Cat.Excrement () simplemente devolver una instancia de RadioActivePoo como Poo? Tienes una interfaz común, úsala. (Y gracias por el ejemplo histérico.)
hometoast
1
Yo también quiero agradecer el ejemplo: @goodgai a continuación sugiere crear Poo abstracto; me pregunto cómo podría explicarle esto a un no programador ...
Paolo Tedesco
1
@Konrad: Ligeramente seguro si debería simplemente reírse de esa brillante comentario, o si también debe preguntar si de hecho hay un olor código muy horrible que debe ser señalado (causa en ese caso me gustaría saber acerca de ella: p)
Svish
3
Estoy casi decepcionado de mí mismo con lo divertidos que encontré estos ejemplos XD
Gurgadurgen

Respuestas:

24

Sé que ya hay muchas soluciones para este problema, pero creo que se me ocurrió una que soluciona los problemas que tenía con las soluciones existentes.

No estaba satisfecho con algunas de las soluciones existentes por las siguientes razones:

  • Primera solución de Paolo Tedesco: Gato y Perro no tienen una clase base común.
  • Segunda solución de Paolo Tedesco: es un poco complicado y difícil de leer.
  • La solución de Daniel Daranas: esto funciona pero saturaría su código con una gran cantidad de declaraciones innecesarias de conversión y Debug.Assert ().
  • Soluciones de hjb417: esta solución no le permite mantener su lógica en una clase base. La lógica es bastante trivial en este ejemplo (llamar a un constructor) pero en un ejemplo del mundo real no lo sería.

Mi solución

Esta solución debería superar todos los problemas que mencioné anteriormente mediante el uso de métodos genéricos y ocultación.

public class Poo { }
public class RadioactivePoo : Poo { }

interface IAnimal
{
    Poo Excrement { get; }
}

public class BaseAnimal<PooType> : IAnimal
    where PooType : Poo, new()
{
    Poo IAnimal.Excrement { get { return (Poo)this.Excrement; } }

    public PooType Excrement
    {
        get { return new PooType(); }
    }
}

public class Dog : BaseAnimal<Poo> { }
public class Cat : BaseAnimal<RadioactivePoo> { }

¡Con esta solución no necesita anular nada en Dog OR Cat! Aquí hay algunos ejemplos de uso:

Cat bruce = new Cat();
IAnimal bruceAsAnimal = bruce as IAnimal;
Console.WriteLine(bruce.Excrement.ToString());
Console.WriteLine(bruceAsAnimal.Excrement.ToString());

Esto dará como resultado: "RadioactivePoo" dos veces, lo que muestra que el polimorfismo no se ha roto.

Otras lecturas

  • Implementación de interfaz explícita
  • nuevo Modificador . No lo usé en esta solución simplificada, pero es posible que lo necesite en una solución más complicada. Por ejemplo, si quisiera crear una interfaz para BaseAnimal, entonces necesitaría usarla en su declinación de "PooType Excrement".
  • out Modificador genérico (covarianza) . De nuevo, no lo MyType<Poo>usé en esta solución, pero si quisiera hacer algo como regresar de IAnimal y regresar MyType<PooType>de BaseAnimal, entonces necesitaría usarlo para poder lanzar entre los dos.
robar
fuente
6
Amigo, esto puede ser genial. No tengo tiempo para analizar más en este momento, pero parece que es posible que lo hayas entendido, y si lo lograste, choca los cinco y gracias por compartir. Desafortunadamente, este asunto de la "caca" y los "excrementos" es una gran distracción.
Nicholas Petersen
1
Creo que encontré una solución para un problema relacionado, donde un método tiene que devolver el tipo heredado, es decir, un método en 'Perro' que fue heredado de 'Animal', sin embargo, devuelve Perro (esto), no Animal. Está hecho con métodos de extensión, podría compartir aquí cuando llegue.
Nicholas Petersen
En cuanto a la seguridad del tipo, bruce.Excement y bruceAsAnimal.Excrement serán del tipo RadioactivePoo ??
Anestis Kivranoglou
@AnestisKivranoglou sí ambos serán de tipo RadioactivePoo
rob
1
He estado arando SO durante 48 horas, y esta respuesta acaba de resolverlo. Y yo estaba como ... "Amigo, esto puede ser genial".
Martin Hansen Lennox
50

¿Qué pasa con una clase base genérica?

public class Poo { }
public class RadioactivePoo : Poo { }

public class BaseAnimal<PooType> 
    where PooType : Poo, new() {
    PooType Excrement {
        get { return new PooType(); }
    }
}

public class Dog : BaseAnimal<Poo> { }
public class Cat : BaseAnimal<RadioactivePoo> { }

EDITAR : Una nueva solución, usando métodos de extensión y una interfaz de marcador ...

public class Poo { }
public class RadioactivePoo : Poo { }

// just a marker interface, to get the poo type
public interface IPooProvider<PooType> { }

// Extension method to get the correct type of excrement
public static class IPooProviderExtension {
    public static PooType StronglyTypedExcrement<PooType>(
        this IPooProvider<PooType> iPooProvider) 
        where PooType : Poo {
        BaseAnimal animal = iPooProvider as BaseAnimal;
        if (null == animal) {
            throw new InvalidArgumentException("iPooProvider must be a BaseAnimal.");
        }
        return (PooType)animal.Excrement;
    }
}

public class BaseAnimal {
    public virtual Poo Excrement {
        get { return new Poo(); }
    }
}

public class Dog : BaseAnimal, IPooProvider<Poo> { }

public class Cat : BaseAnimal, IPooProvider<RadioactivePoo> {
    public override Poo Excrement {
        get { return new RadioactivePoo(); }
    }
}

class Program { 
    static void Main(string[] args) {
        Dog dog = new Dog();
        Poo dogPoo = dog.Excrement;

        Cat cat = new Cat();
        RadioactivePoo catPoo = cat.StronglyTypedExcrement();
    }
}

De esta manera, el perro y el gato heredan de Animal (como se comentó en los comentarios, mi primera solución no preservó la herencia).
Es necesario marcar explícitamente las clases con la interfaz del marcador, lo cual es doloroso, pero tal vez esto pueda darte algunas ideas ...

SEGUNDA EDICIÓN @Svish: Modifiqué el código para mostrar explícitamente que el método de extensión no aplica de ninguna manera el hecho de que iPooProviderhereda BaseAnimal. ¿Qué quieres decir con "aún más tipado"?

Paolo Tedesco
fuente
Estaba pensando lo mismo.
Doctor Jones
9
¿No pierdes el polimorfismo entre perro y gato?
almog.ori
¿Qué pasaría si hubiera otros tipos que también quisiera anular? Técnicamente, podrías terminar con un montón de argumentos de tipo. Lo que creo que podría haberme encontrado molesto ... pero sí, esta es una solución.
Svish
@Svish: Me imagino que una vez que eso suceda, es hora de usar un marco de inyección de dependencia.
Brian
¿Cómo sabe el método StronglyTypedExcrement que iPooProvider es un BaseAnimal? ¿Simplemente adivina? ¿O es algo que no veo? ¿Sería posible hacer que ese método fuera aún más tipado?
Svish
32

Esto se llama covarianza de tipo de retorno y no es compatible con C # o .NET en general, a pesar de los deseos de algunas personas .

Lo que haría es mantener la misma firma pero agregar una ENSUREcláusula adicional a la clase derivada en la que me aseguro de que esta devuelva un RadioActivePoo. Entonces, en resumen, haría a través del diseño por contrato lo que no puedo hacer a través de la sintaxis.

Otros prefieren fingirlo . Está bien, supongo, pero tiendo a economizar líneas de código de "infraestructura". Si la semántica del código es lo suficientemente clara, estoy contento y el diseño por contrato me permite lograrlo, aunque no es un mecanismo de tiempo de compilación.

Lo mismo para los genéricos, que sugieren otras respuestas . Los usaría por una mejor razón que simplemente devolver caca radiactiva, pero ese soy solo yo.

Daniel Daranas
fuente
¿Qué es la cláusula ENSURE? ¿Cómo funcionaría eso? ¿Es un atributo en .Net?
Svish
1
En .Net y antes de ver los contratos de código de .Net 4.0, escribo cláusulas ENSURE (x) simplemente como "Debug.Assert (x)". Para obtener más referencias, consulte, por ejemplo, archive.eiffel.com/doc/manuals/technology/contract/page.html o Object Oriented Software Construction, 2nd Edition, por Bertrand Meyer (1994) capítulo 11.
Daniel Daranas
5
"Los usaría por una mejor razón que simplemente devolver caca radiactiva, pero soy solo yo" pertenece a mi lista de citas favoritas :)
Daniel Daranas
9

También existe esta opción (implementación de interfaz explícita)

public class Cat:Animal
{
  Poo Animal.Excrement { get { return Excrement; } }
  public RadioactivePoo Excrement { get { return new RadioactivePoo(); } }
}

Pierdes la capacidad de usar la clase base para implementar Cat, pero en el lado positivo, mantienes el polimorfismo entre Cat y Dog.

Pero dudo que la complejidad adicional valga la pena.

Rasmus Faber
fuente
4

¿Por qué no definir un método virtual protegido que cree el 'Excremento' y mantener la propiedad pública que devuelve el 'Excremento' no virtual? Luego, las clases derivadas pueden anular el tipo de retorno de la clase base.

En el siguiente ejemplo, hago que 'Excrement' no sea virtual, pero proporciono la propiedad ExcrementImpl para permitir que las clases derivadas proporcionen el 'Poo' adecuado. Los tipos derivados pueden anular el tipo de retorno de 'Excremento' ocultando la implementación de la clase base.

Ex:

namepace ConsoleApplication8

{
public class Poo { }

public class RadioactivePoo : Poo { }

public interface Animal
{
    Poo Excrement { get; }
}

public class AnimalBase
{
    public Poo Excrement { get { return ExcrementImpl; } }

    protected virtual Poo ExcrementImpl
    {
        get { return new Poo(); }
    }
}

public class Dog : AnimalBase
{
    // No override, just return normal poo like normal animal
}

public class Cat : AnimalBase
{
    protected override Poo ExcrementImpl
    {
        get { return new RadioactivePoo(); }
    }

    public new RadioactivePoo Excrement { get { return (RadioactivePoo)ExcrementImpl; } }
}
}
Hasani Blackwell
fuente
el ejemplo hizo que esto fuera mucho más difícil de entender. ¡pero gran código!
rposky
2

Corrígeme si me equivoco, pero no es todo el punto del polimorfismo para poder devolver RadioActivePoo si hereda de Poo, el contrato sería el mismo que la clase abstracta pero solo devolver RadioActivePoo ()

almog.ori
fuente
2
Tienes toda la razón, pero él quiere evitar el elenco adicional y algunos tipos más fuertes, que es principalmente para lo que son los genéricos ...
Paolo Tedesco
2

Prueba esto:

namespace ClassLibrary1
{
    public interface Animal
    {   
        Poo Excrement { get; }
    }

    public class Poo
    {
    }

    public class RadioactivePoo
    {
    }

    public class AnimalBase<T>
    {   
        public virtual T Excrement
        { 
            get { return default(T); } 
        }
    }


    public class Dog : AnimalBase<Poo>
    {  
        // No override, just return normal poo like normal animal
    }

    public class Cat : AnimalBase<RadioactivePoo>
    {  
        public override RadioactivePoo Excrement 
        {
            get { return new RadioactivePoo(); } 
        }
    }
}
Shiraz Bhaiji
fuente
¿Cuál es el punto de la interfaz Animal aquí? Nada hereda de ella.
robar el
1

Creo que he encontrado una forma que no depende de métodos genéricos o de extensión, sino de la ocultación de métodos. Sin embargo, puede romper el polimorfismo, así que tenga especial cuidado si sigue heredando de Cat.

Espero que esta publicación pueda ayudar a alguien, a pesar de tener un retraso de 8 meses.

public interface Animal
{
    Poo Excrement { get; }
}

public class Poo
{
}

public class RadioActivePoo : Poo
{
}

public class AnimalBase : Animal
{
    public virtual Poo Excrement { get { return new Poo(); } }
}

public class Dog : AnimalBase
{
    // No override, just return normal poo like normal animal
}

public class CatBase : AnimalBase
{
    public override Poo Excrement { get { return new RadioActivePoo(); } }
}

public class Cat : CatBase
{
    public new RadioActivePoo Excrement { get { return (RadioActivePoo) base.Excrement; } }
}
Cybis
fuente
Aack, no importa. No me di cuenta de que hjb417 ya había publicado una solución similar. Al menos el mío no requiere modificar la clase base.
Cybis
su "solución" HACE polimorfismo de descanso, por lo que no es realmente una solución. Por otro lado, la solución hjb es una solución real y, en mi humilde opinión, bastante inteligente.
greenoldman
0

Podría ayudar si RadioactivePoo se deriva de la caca y luego usa genéricos.

bobbyalex
fuente
0

FYI. Esto se implementa con bastante facilidad en Scala.

trait Path

trait Resource
{
    def copyTo(p: Path): Resource
}
class File extends Resource
{
    override def copyTo(p: Path): File = new File
    override def toString = "File"
}
class Directory extends Resource
{
    override def copyTo(p: Path): Directory = new Directory
    override def toString = "Directory"
}

val test: Resource = new Directory()
test.copyTo(null)

Aquí hay un ejemplo en vivo con el que puedes jugar: http://www.scalakata.com/50d0d6e7e4b0a825d655e832

jedesah
fuente
0

Creo que tu respuesta se llama covarianza.

class Program
{
    public class Poo
    {
        public virtual string Name { get{ return "Poo"; } }
    }

    public class RadioactivePoo : Poo
    {
        public override string Name { get { return "RadioactivePoo"; } }
        public string DecayPeriod { get { return "Long time"; } }
    }

    public interface IAnimal<out T> where T : Poo
    {
        T Excrement { get; }
    }

    public class Animal<T>:IAnimal<T> where T : Poo 
    {
        public T Excrement { get { return _excrement ?? (_excrement = (T) Activator.CreateInstance(typeof (T), new object[] {})); } } 
        private T _excrement;
    }

    public class Dog : Animal<Poo>{}
    public class Cat : Animal<RadioactivePoo>{}

    static void Main(string[] args)
    {
        var dog = new Dog();
        var cat = new Cat();

        IAnimal<Poo> animal1 = dog;
        IAnimal<Poo> animal2 = cat;

        Poo dogPoo = dog.Excrement;
        //RadioactivePoo dogPoo2 = dog.Excrement; // Error, dog poo is not RadioactivePoo.

        Poo catPoo = cat.Excrement;
        RadioactivePoo catPoo2 = cat.Excrement;

        Poo animal1Poo = animal1.Excrement;
        Poo animal2Poo = animal2.Excrement;
        //RadioactivePoo animal2RadioactivePoo = animal2.Excrement; // Error, IAnimal<Poo> reference do not know better.


        Console.WriteLine("Dog poo name: {0}",dogPoo.Name);
        Console.WriteLine("Cat poo name: {0}, decay period: {1}" ,catPoo.Name, catPoo2.DecayPeriod);
        Console.WriteLine("Press any key");

        var key = Console.ReadKey();
    }
}
Pierre Grondin
fuente
0

Podría usar una devolución de una interfaz. En tu caso, IPoo.

Esto es preferible a usar un tipo genérico, en su caso, porque está usando una clase de base de comentarios.

David Shaskin
fuente
0

A continuación, se combinan algunos de los mejores aspectos de varias otras respuestas, así como una técnica para permitir el aspecto clave de Cattener una Excrementpropiedad del RadioactivePootipo requerido , pero poder devolver eso simplemente como Poosi solo supiéramos que tenemos un en AnimalBaselugar de específicamente unCat .

No se requiere que los llamantes usen genéricos, a pesar de que estén presentes en las implementaciones, ni que llamen a una función con un nombre diferente para ser especial Poo.

La clase intermedia AnimalWithSpecialisationssolo sirve para sellar la Excrementpropiedad, conectándola a través de una SpecialPoopropiedad no pública a la clase derivada AnimalWithSpecialPoo<TPoo>que tiene unExcrement propiedad de un tipo de retorno derivado.

Si Cates el único animal que Pooes especial de alguna manera, o no queremos que el tipo de Excrementsea ​​la característica definitoria principal de a Cat, la clase genérica intermedia podría omitirse en la jerarquía, por lo que se Catderiva directamente de AnimalWithSpecialisations, pero si hay son varios animales diferentes cuya característica principal es que Pooson especiales de alguna manera, separar la "placa de caldera" en clases intermedias ayuda a mantener elCat clase en sí bastante limpia, aunque a costa de un par de llamadas a funciones virtuales adicionales.

El código de ejemplo muestra que la mayoría de las operaciones esperadas funcionan "como se esperaba".

public interface IExcretePoo<out TPoo>
  where TPoo : Poo
{
  TPoo Excrement { get; }
}

public class Poo
{ }

public class RadioactivePoo : Poo
{ }

public class AnimalBase : IExcretePoo<Poo>
{
  public virtual Poo Excrement { get { return new Poo(); } }
}

public class Dog : AnimalBase
{
  // No override, just return normal poo like normal animal
}

public abstract class AnimalWithSpecialisations : AnimalBase
{
  // this class connects AnimalBase to AnimalWithSpecialPoo<TPoo>
  public sealed override Poo Excrement { get { return SpecialPoo; } }

  // if not overridden, our "special" poo turns out just to be normal animal poo...
  protected virtual Poo SpecialPoo { get { return base.Excrement; } }
}

public abstract class AnimalWithSpecialPoo<TPoo> : AnimalWithSpecialisations, IExcretePoo<TPoo>
  where TPoo : Poo
{
  sealed protected override Poo SpecialPoo { get { return Excrement; } }
  public new abstract TPoo Excrement { get; }
}

public class Cat : AnimalWithSpecialPoo<RadioactivePoo>
{
  public override RadioactivePoo Excrement { get { return new RadioactivePoo(); } }
}

class Program
{
  static void Main(string[] args)
  {
    Dog dog = new Dog();
    Poo dogPoo = dog.Excrement;

    Cat cat = new Cat();
    RadioactivePoo catPoo = cat.Excrement;

    AnimalBase animal = cat;

    Poo animalPoo = catPoo;
    animalPoo = animal.Excrement;

    AnimalWithSpecialPoo<RadioactivePoo> radioactivePooingAnimal = cat;
    RadioactivePoo radioactivePoo = radioactivePooingAnimal.Excrement;

    IExcretePoo<Poo> pooExcreter = cat; // through this interface we don't know the Poo was radioactive.
    IExcretePoo<RadioactivePoo> radioactivePooExcreter = cat; // through this interface we do.

    // we can replace these with the dog equivalents:
    animal = dog;
    animalPoo = dogPoo;
    pooExcreter = dog;

    // but we can't do:
    // radioactivePooExcreter = dog;
    // radioactivePooingAnimal = dog;
    // radioactivePoo = dogPoo;
  }
Steve
fuente
0

C # 9 nos da tipos de retorno de anulación covariantes. Básicamente: lo que quieres simplemente funciona .

Marc Gravell
fuente
-1

Bueno, en realidad es posible devolver un Tipo concreto que varía del Tipo de retorno heredado (incluso para métodos estáticos), gracias a dynamic:

public abstract class DynamicBaseClass
{
    public static dynamic Get (int id) { throw new NotImplementedException(); }
}

public abstract class BaseClass : DynamicBaseClass
{
    public static new BaseClass Get (int id) { return new BaseClass(id); }
}

public abstract class DefinitiveClass : BaseClass
{
    public static new DefinitiveClass Get (int id) { return new DefinitiveClass(id);
}

public class Test
{
    public static void Main()
    {
        var testBase = BaseClass.Get(5);
        // No cast required, IntelliSense will even tell you
        // that var is of type DefinitiveClass
        var testDefinitive = DefinitiveClass.Get(10);
    }
}

Implementé esto en un contenedor de API que escribí para mi empresa. Si planea desarrollar una API, esto tiene potencial para mejorar la usabilidad y la experiencia de desarrollo en algunos casos de uso. Sin embargo, el uso de dynamictiene un impacto en el rendimiento, así que trate de evitarlo.

wobuntu
fuente