C #: zastępowanie zwracanych typów


81

Czy istnieje sposób na zastąpienie zwracanych typów w C #? Jeśli tak, to jak, a jeśli nie, dlaczego i jaki jest zalecany sposób?

Mój przypadek jest taki, że mam interfejs z abstrakcyjną klasą bazową i jej potomkami. Chciałbym to zrobić (ok niezupełnie, ale jako przykład!):

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

RadioactivePoooczywiście dziedziczy z Poo.

Powodem, dla którego chcę tego jest, aby ci, którzy używają Catprzedmiotów, mogli korzystać z tej Excrementwłaściwości bez konieczności rzucania Poodo, RadioactivePoopodczas gdy na przykład Catmoże nadal być częścią Animallisty, na której użytkownicy niekoniecznie muszą być świadomi lub dbać o swoją radioaktywną kupę. Mam nadzieję, że to miało sens ...

O ile widzę, kompilator przynajmniej na to nie pozwala. Więc myślę, że to niemożliwe. Ale co byś polecił jako rozwiązanie tego problemu?


2
A co z lekami generycznymi? Czy nie pomogliby?
Arnis Lapsa

3
Czy Cat.Excrement () nie powinien po prostu zwrócić i wystąpienie RadioActivePoo jako Poo? Masz wspólny interfejs, użyj go. (I dzięki za histeryczny przykład.)
Hometoast

1
Ja też chcę podziękować za przykład: @goodgai poniżej sugeruje stworzenie abstrakcyjnej Poo - zastanawiam się, jak mógłbym to wyjaśnić nie-programiście ...
Paolo Tedesco

1
@Konrad: Nie jestem pewien, czy powinienem się tylko śmiać z tego genialnego komentarza, czy też powinienem zapytać, czy rzeczywiście jest to bardzo okropny zapach kodu, na który należy zwrócić uwagę (bo w takim przypadku chciałbym o tym wiedzieć: p)
Svish

3
Jestem prawie rozczarowany tym, jak zabawne okazały się te przykłady. XD
Gurgadurgen

Odpowiedzi:


24

Wiem, że istnieje już wiele rozwiązań tego problemu, ale myślę, że wymyśliłem takie, które rozwiązuje problemy, które miałem z istniejącymi rozwiązaniami.

Nie byłem zadowolony z niektórych istniejących rozwiązań z następujących powodów:

  • Pierwsze rozwiązanie Paolo Tedesco: Kot i pies nie mają wspólnej klasy bazowej.
  • Drugie rozwiązanie Paolo Tedesco: jest nieco skomplikowane i trudne do odczytania.
  • Rozwiązanie Daniela Daranasa: To działa, ale zagraciłoby twój kod dużą ilością niepotrzebnych instrukcji rzutowania i Debug.Assert ().
  • Rozwiązania hjb417: To rozwiązanie nie pozwala na utrzymanie logiki w klasie bazowej. W tym przykładzie logika jest dość trywialna (wywołanie konstruktora), ale w prawdziwym świecie nie byłoby.

Moje rozwiązanie

To rozwiązanie powinno przezwyciężyć wszystkie problemy, o których wspomniałem powyżej, stosując zarówno typy ogólne, jak i ukrywanie metod.

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

Dzięki temu rozwiązaniu nie musisz niczego zastępować w Dog OR Cat! Oto kilka przykładowych zastosowań:

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

Spowoduje to dwukrotne wyświetlenie: „RadioactivePoo”, co oznacza, że ​​polimorfizm nie został przerwany.

Dalsze czytanie

  • Jawna implementacja interfejsu
  • nowy modyfikator . Nie użyłem go w tym uproszczonym rozwiązaniu, ale możesz go potrzebować w bardziej skomplikowanym rozwiązaniu. Na przykład, jeśli chcesz stworzyć interfejs dla BaseAnimal, musisz go użyć w swojej dekleracji "PooType Excrement".
  • out Ogólny modyfikator (kowariancja) . Ponownie nie użyłem tego w tym rozwiązaniu, ale jeśli chcesz zrobić coś takiego jak powrót MyType<Poo>z IAnimal i powrót MyType<PooType>z BaseAnimal, musisz go użyć, aby móc rzucać między nimi.

6
Stary, to może być super fajne. W tej chwili nie mam czasu na dalsze analizowanie, ale wygląda na to, że możesz to mieć, a jeśli to złamałeś, przybij piątkę i dziękuję za udostępnienie. Niestety ten biznes „kupy” i „ekskrementów” jest dużym rozproszeniem.
Nicholas Petersen

1
Myślę, że znalazłem rozwiązanie podobnego problemu, w którym metoda musi zwrócić odziedziczony typ - tj. Metoda w „Dog”, która została odziedziczona po „Animal”, mimo to zwraca Dog (this), a nie Animal. Zrobiono to za pomocą metod rozszerzających, które mogę udostępnić tutaj, kiedy do tego dojdę.
Nicholas Petersen

Jeśli chodzi o bezpieczeństwo typu, zarówno bruce.Excement i bruceAsAnimal.Excrement będą typu RadioactivePoo ??
Anestis Kivranoglou

@AnestisKivranoglou tak, oba będą typu RadioactivePoo
rob

1
Orzełem tak przez 48 godzin, a ta odpowiedź właśnie mnie złamała. A ja na to… „Stary, to może być super fajne”.
Martin Hansen Lennox

50

A co z ogólną klasą bazową?

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

EDYCJA : Nowe rozwiązanie, wykorzystujące metody rozszerzające i interfejs znaczników ...

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

W ten sposób Pies i Kot dziedziczą po Animal (jak zauważono w komentarzach, moje pierwsze rozwiązanie nie zachowało dziedziczenia).
Konieczne jest wyraźne oznaczenie klas za pomocą interfejsu znaczników, co jest bolesne, ale może to może dać ci kilka pomysłów ...

DRUGA EDYCJA @Svish: Zmodyfikowałem kod, aby wyraźnie pokazać, że metoda rozszerzenia nie wymusza w żaden sposób faktu, że iPooProviderdziedziczy po BaseAnimal. Co masz na myśli, mówiąc „jeszcze silniej wpisane na maszynie”?


Właśnie myślałem o tym samym.
Doctor Jones,

9
Czy nie tracisz na polimorfizmie między psem a kotem?
almog.ori

A co by było, gdyby były inne typy, które również chciałbyś zastąpić? Technicznie możesz skończyć z całą masą argumentów typu. Co, jak sądzę, mogło mnie denerwować ... ale tak, to jest rozwiązanie.
Svish

@Svish: Wyobrażam sobie, że kiedy to się stanie, nadszedł czas, aby użyć frameworka wstrzykiwania zależności.
Brian

Skąd metoda StronglyTypedExcrement wie, że iPooProvider to BaseAnimal? Czy to tylko zgadywanie? Czy jest to coś, czego nie widzę? Czy byłoby możliwe, aby ta metoda była jeszcze silniej wpisywana?
Svish

32

Nazywa się to kowariancją typu zwracanego i nie jest ogólnie obsługiwane w językach C # ani .NET, pomimo życzeń niektórych osób .

Chciałbym zachować ten sam podpis, ale dodać dodatkową ENSUREklauzulę do klasy pochodnej, w której upewniam się, że ta zwraca plik RadioActivePoo. Krótko mówiąc, wykonałbym przez projekt na podstawie umowy to, czego nie mogę zrobić za pomocą składni.

Inni wolą to udawać . Myślę, że to w porządku, ale staram się oszczędzać linie kodu związane z infrastrukturą. Jeśli semantyka kodu jest wystarczająco jasna, cieszę się, a projektowanie na podstawie umowy pozwala mi to osiągnąć, chociaż nie jest to mechanizm czasu kompilacji.

To samo dotyczy leków generycznych, co sugerują inne odpowiedzi . Użyłbym ich z lepszego powodu niż po prostu zwracanie radioaktywnej kupy - ale to tylko ja.


Co to jest klauzula ENSURE? Jak by to działało? Czy jest to atrybut w .Net?
Svish

1
W .Net i przed obejrzeniem kontraktów Code .Net 4.0, piszę klauzule ENSURE (x) jako po prostu „Debug.Assert (x)”. Więcej informacji można znaleźć na przykład w archive.eiffel.com/doc/manuals/technology/contract/page.html lub Object Oriented Software Construction, 2nd Edition, Bertrand Meyer (1994) rozdział 11.
Daniel Daranas

5
Użyłbym

9

Istnieje również ta opcja (jawna implementacja interfejsu)

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

Tracisz możliwość użycia klasy bazowej do implementacji Cat, ale z drugiej strony zachowujesz polimorfizm między Cat i Dog.

Ale wątpię, że dodatkowa złożoność jest tego warta.


4

Dlaczego nie zdefiniować chronionej metody wirtualnej, która tworzy „odchody” i zachowuje właściwość publiczną, która zwraca niewirtualne „odchody”. Następnie klasy pochodne mogą przesłonić zwracany typ klasy bazowej.

W poniższym przykładzie ustawiam „Excrement” jako niewirtualny, ale udostępniam właściwość ExcrementImpl, aby umożliwić klasom pochodnym dostarczanie odpowiedniego „Poo”. Typy pochodne mogą następnie przesłonić zwracany typ „Excrement”, ukrywając implementację klasy bazowej.

Dawny:

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

przykład uczynił to o wiele trudniejszym do zrozumienia. ale świetny kod!
rposky

2

Popraw mnie, jeśli się mylę, ale nie chodzi o to, aby móc zwrócić RadioActivePoo, jeśli dziedziczy on po Poo, kontrakt byłby taki sam jak klasa abstrakcyjna, ale po prostu zwróciłby RadioActivePoo ()


2
Masz całkowitą rację, ale on chce uniknąć dodatkowej obsady i silniejszego pisania na klawiaturze, do czego służą generyczne ...
Paolo Tedesco.

2

Spróbuj tego:

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

Jaki jest tutaj sens interfejsu Animal? Nic z tego nie dziedziczy.
okradnij

1

Myślę, że znalazłem sposób, który nie zależy od typów ogólnych lub metod rozszerzających, ale raczej od ukrywania metod. Może jednak złamać polimorfizm, więc zachowaj szczególną ostrożność, jeśli dalej odziedziczysz po Cat.

Mam nadzieję, że ten post nadal może komuś pomóc, pomimo spóźnienia o 8 miesięcy.

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

Aack, nieważne. Nie zdawałem sobie sprawy, że hjb417 już opublikował podobne rozwiązanie. Przynajmniej moja nie wymaga modyfikacji klasy bazowej.
Cybis

Twoje „rozwiązanie” NIE przełamuje polimorfizmu, więc tak naprawdę nie jest rozwiązaniem. Z drugiej strony rozwiązanie hjb to prawdziwe rozwiązanie i całkiem sprytne IMHO.
greenoldman

0

Może pomóc, jeśli RadioactivePoo pochodzi z poo, a następnie użyje leków generycznych.


0

FYI. Jest to dość łatwe w Scali.

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)

Oto przykład na żywo, z którym możesz grać: http://www.scalakata.com/50d0d6e7e4b0a825d655e832


0

Uważam, że twoja odpowiedź nazywa się kowariancją.

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

0

Możesz po prostu użyć zwrotu Interface. W twoim przypadku IPoo.

W twoim przypadku jest to lepsze niż używanie typu ogólnego, ponieważ używasz klasy bazowej komentarza.


0

Poniższe łączy niektóre z najlepszych aspektów kilku innych odpowiedzi, a także technikę umożliwiającą kluczowy aspekt Catposiadania Excrementwłaściwości wymaganego RadioactivePootypu, ale możliwość zwrócenia jej tak Poo, jakbyśmy tylko wiedzieli, że mamy AnimalBaseraczej niż konkretnie aCat .

Obiekt wywołujący nie musi używać typów ogólnych, mimo że są one obecne w implementacjach, ani wywoływać funkcji o innych nazwach w celu uzyskania specjalnego Poo.

Klasa pośrednia AnimalWithSpecialisationssłuży jedynie do zapieczętowania Excrementwłasności, łącząc ją poprzez niepubliczną SpecialPoowłaściwość z klasą pochodną, AnimalWithSpecialPoo<TPoo>która ma Excrementwłaściwość pochodnego typu zwracanego.

Jeśli Catjest to jedyne zwierzę, które Poojest w jakikolwiek sposób wyjątkowe, lub nie chcemy, aby typ Excrementbył główną cechą definiującą a Cat, pośrednia klasa ogólna może zostać pominięta w hierarchii, tak że Catpochodzi bezpośrednio z AnimalWithSpecialisations, ale jeśli istnieje istnieje kilka różnych zwierząt, których podstawową cechą jest to, że Poosą one w jakiś sposób wyjątkowe, rozdzielenie „kotłowni” na klasy pośrednie pomaga utrzymać Catsamą klasę w dość czystym stanie, aczkolwiek kosztem kilku dodatkowych wywołań funkcji wirtualnych.

Przykładowy kod pokazuje, że większość oczekiwanych operacji działa „zgodnie z oczekiwaniami”.

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

0

C # 9 daje nam kowariantne zastępowanie zwracanych typów. Zasadniczo: to, czego chcesz, po prostu działa .


-1

Cóż, w rzeczywistości możliwe jest zwrócenie konkretnego Type, który różni się od odziedziczonego zwracanego Type (nawet w przypadku metod statycznych), dzięki 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);
    }
}

Zaimplementowałem to w opakowaniu API, które napisałem dla mojej firmy. Jeśli planujesz opracować interfejs API, może to poprawić użyteczność i doświadczenie deweloperskie w niektórych przypadkach użycia. Niemniej jednak użycie dynamicma wpływ na wydajność, więc staraj się tego unikać.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.