C #: Заміщення типів повернення


81

Чи є спосіб замінити типи повернення в C #? Якщо так, то як, а якщо ні, чому і який рекомендований спосіб зробити це?

Моя справа полягає в тому, що я маю інтерфейс з абстрактним базовим класом і нащадками цього. Я хотів би зробити це (добре не дуже, але як приклад!):

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

RadioactivePooзвичайно успадковує від Poo.

Моя причина для бажання це так , що ті , хто використовує Catоб'єкти можуть використовувати Excrementвластивість без закидати Pooв RadioactivePooтой час як, наприклад, Catвсе ще може бути частиною Animalсписку , де користувачі не можуть обов'язково бути в курсі чи піклування про їх радіоактивної мочитися. Сподіваюся, це мало сенс ...

Наскільки я бачу, компілятор не дозволяє цього принаймні. Тож я здогадуюсь, що це неможливо. Але що б ви порадили як рішення цього?


2
А як щодо дженериків? Чи не допомогли б вони?
Арніс Лапса,

3
Чи не повинен Cat.Excrement () просто повернути та екземпляр RadioActivePoo як Poo? У вас спільний інтерфейс, використовуйте його. (І дякую за істеричний приклад.)
рідне місце

1
Я теж хочу подякувати за приклад: @goodgai нижче пропонує створити абстрактний Poo - цікаво, як я міг пояснити це непрограмісту ...
Паоло Тедеско

1
@Konrad: Трохи не впевнений, чи варто мені просто посміятися над цим блискучим коментарем, чи не запитати, чи насправді це дуже жахливий запах коду, на який слід вказати (бо в такому випадку я хотів би про це знати: р)
Свіш

3
Я майже розчарований у собі тим, як смішно мені
здалися

Відповіді:


24

Я знаю, що для цієї проблеми вже є багато рішень, але, думаю, я придумав такий, який виправляє проблеми, які виникали у мене з існуючими рішеннями.

Я не був задоволений деякими існуючими рішеннями з таких причин:

  • Перше рішення Паоло Тедеско: Кіт і Собака не мають загального базового класу.
  • Друге рішення Паоло Тедеско: Це дещо складне і важке для читання.
  • Рішення Даніеля Даранаса: Це працює, але це загрожує вашим кодом безліччю непотрібних кастингів та інструкцій Debug.Assert ().
  • Рішення hjb417: Це рішення не дозволяє вам зберігати свою логіку в базовому класі. У цьому прикладі логіка досить тривіальна (виклик конструктора), але в реальному прикладі цього не було б.

Моє рішення

Це рішення має подолати всі проблеми, про які я згадав вище, використовуючи як загальні засоби, так і приховування методів.

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

За допомогою цього рішення не потрібно нічого перевизначати в програмі Dog OR Cat! Ось декілька зразків використання:

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

Це видасть: "RadioactivePoo" двічі, що показує, що поліморфізм не порушений.

Подальше читання

  • Явна реалізація інтерфейсу
  • новий Модифікатор . Я не використовував його в цьому спрощеному рішенні, але вам може знадобитися в більш складному рішенні. Наприклад, якщо ви хочете створити інтерфейс для BaseAnimal, то вам доведеться використовувати його у своєму відмінні "Екскрементів PooType".
  • вихід Загальний модифікатор (коваріація) . Знову ж таки, я не використовував його в цьому рішенні, але якщо ви хочете зробити щось на зразок повернення MyType<Poo>від IAnimal і повернення MyType<PooType>від BaseAnimal, то вам потрібно буде використовувати його, щоб мати можливість робити кастинг між ними.

6
Чувак, це може бути супер круто. Наразі я не маю часу для подальшого аналізу, але схоже, ви, можливо, це зрозуміли, і якщо ви все-таки зламали це, п’ятірка і дякую за поділ. На жаль, цей бізнес, пов’язаний із “каком” та “екскрементами”, сильно відволікає увагу.
Ніколас Петерсен

1
Думаю, я знайшов вирішення пов'язаної з цим проблеми, коли метод повинен повертати успадкований тип - тобто метод у 'Dog', який був успадкований від 'Animal', тим не менше повертає Dog (this), а не Animal. Це зроблено за допомогою методів розширення, я можу поділитися тут, коли доберусь до нього.
Ніколас Петерсен

Що стосується безпеки типу, це буде як bruce.Excement, так і bruceAsAnimal. Екскременти будуть типу RadioactivePoo ??
Anestis Kivranoglou

@AnestisKivranoglou так, обидва будуть типу RadioactivePoo
грабувати

1
Я орав SO SO 48 годин, і ця відповідь просто зламала його. І я був схожий на ... "Чувак, це може бути супер круто".
Martin Hansen Lennox

50

А як щодо загального базового класу?

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

EDIT : нове рішення, що використовує методи розширення та інтерфейс маркера ...

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

Таким чином, Собака і Кіт успадковують тварину (як зазначалося в коментарях, моє перше рішення не зберегло спадщину).
Потрібно чітко позначити класи за допомогою інтерфейсу маркера, що боляче, але, можливо, це може дати вам кілька ідей ...

ДРУГЕ РЕДАКТУВАННЯ @Svish: Я змінив код, щоб чітко показати, що метод розширення жодним чином не примушує до того, що iPooProviderуспадковується від BaseAnimal. Що ви маєте на увазі під "ще сильнішим типом"?


Просто думав про те саме.
Доктор Джонс,

9
Чи не втрачаєте ви на поліморфізмі між Собакою та Кішкою?
almog.ori

Що, якби були й інші типи, які ви також хотіли б замінити? Технічно ви могли б отримати цілу купу аргументів типу. Що, на мою думку, мене могло б роздратувати ... але так, це рішення.
Свіш

@Svish: Я думаю, що коли це трапиться, настав час використовувати структуру ін'єкції залежностей.
Брайан

Звідки метод StronglyTypedExcrement знає, що iPooProvider - це BaseAnimal? Це просто здогадується? Або це щось я не бачу? Чи можна було б зробити цей метод ще сильніше набраним?
Свіш

32

Це називається коваріацією типу повернення і не підтримується в C # або .NET загалом, незважаючи на бажання деяких людей .

Що б я зробив, це зберегти той самий підпис, але додати додаткове ENSUREречення до похідного класу, в якому я б переконався, що цей повертає aRadioActivePoo . Отже, коротше кажучи, я б робив за проектом за контрактом те, чого не можу робити за допомогою синтаксису.

Інші воліють підробляти замість цього . Гадаю, це нормально, але я прагну економії "інфраструктурних" рядків коду. Якщо семантика коду досить чітка, я щасливий, і дизайн за контрактом дозволяє мені досягти цього, хоча це не механізм часу компіляції.

Те саме стосується дженериків, на які пропонують інші відповіді . Я б скористався ними з кращої причини, ніж просто повернення радіоактивних виділень - але це лише я.


Що таке речення ENSURE? Як би це працювало? Це атрибут у .Net?
Свіш

1
У .Net і перед тим, як побачити кодові контракти .Net 4.0, я пишу речення ENSURE (x) просто "Debug.Assert (x)". Додаткові посилання див., Наприклад, archive.eiffel.com/doc/manuals/technology/contract/page.html або Об'єктно-орієнтована конструкція програмного забезпечення, 2-е видання, автор Бертран Майер (1994), глава 11.
Даніель Даранас,

5
"Я б використав їх з кращої причини, ніж просто повернення радіоактивних фекалій - але це лише я", належить до мого списку улюблених власних цитат :)
Даніель Даранас,

9

Існує також цей варіант (явна реалізація інтерфейсу)

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

Ви втрачаєте можливість використовувати базовий клас для реалізації Cat, але плюсом є збереження поліморфізму між Cat і Dog.

Але я сумніваюся, що додаткова складність того варта.


4

Чому б не визначити захищений віртуальний метод, який створює екскременти, і не зберегти загальнодоступне властивість, яке повертає невіртуальний екскремент. Тоді похідні класи можуть замінити тип повернення базового класу.

У наступному прикладі я роблю 'Excrement' невіртуальним, але надаю властивість ExcrementImpl, щоб дозволені похідні класи надавали належний 'Poo'. Потім похідні типи можуть замінити тип повернення 'Excrement', приховуючи реалізацію базового класу.

Приклад:

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

приклад зробив це набагато важчим для розуміння. але чудовий код!
rposky

2

Виправте мене, якщо помиляюсь, але чи не вся суть поліліморфізму в тому, щоб мати можливість повернути RadioActivePoo, якщо він успадкує від Poo, контракт буде таким же, як абстрактний клас, але просто поверне RadioActivePoo ()


2
Ви цілком праві, але він хоче уникнути зайвого акторського складу та деяких сильніших типів, для чого в основному і потрібні дженерики ...
Паоло Тедеско,

2

Спробуйте це:

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

У чому тут сенс інтерфейсу Animal? Від цього нічого не успадковується.
грабувати

1

Думаю, я знайшов спосіб, який не залежить від загальних методів або методів розширення, а швидше від приховування методів. Однак це може порушити поліморфізм, тому будьте особливо обережні, якщо надалі отримуватимете спадок від Кішки.

Сподіваюсь, ця публікація все ж може комусь допомогти, незважаючи на запізнення на 8 місяців.

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

Так, неважливо. Я не розумів, що hjb417 вже розміщував подібне рішення. Принаймні мій не вимагає модифікації базового класу.
Cybis

ваше "рішення" ЛЮБИТЬ поліморфізм, тому насправді це не рішення. З іншого боку, рішення hjb є реальним рішенням і досить розумним IMHO.
greenoldman

0

Це може допомогти, якщо RadioactivePoo походить від poo, а потім використовує дженерики.


0

FYI. Це досить легко реалізується в 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)

Ось живий приклад, з яким можна грати: http://www.scalakata.com/50d0d6e7e4b0a825d655e832


0

Я вважаю, що ваша відповідь називається коваріацією.

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

Ви можете просто використовувати інтерфейс повернення. У вашому випадку IPoo.

У вашому випадку це переважно, ніж використання загального типу, оскільки ви використовуєте базовий клас коментарів.


0

Далі поєднуються деякі найкращі аспекти кількох інших відповідей, а також техніка, яка дозволяє визначити ключовий аспект Catнаявності Excrementвластивості необхідного RadioactivePooтипу, але можливості повернути це як просто, Pooякщо ми лише знаємо, що маємо, AnimalBaseа не зокрема aCat .

Від абонентів не вимагається використовувати загальні засоби, хоча вони присутні в реалізаціях, а також не викликати інакше названу функцію для отримання спеціальних Poo .

Проміжний клас AnimalWithSpecialisationsслужить лише для герметизації Excrementвластивості, з'єднуючи його через непублічну SpecialPooвластивість з похідним класом, AnimalWithSpecialPoo<TPoo>який маєExcrement властивість похідного типу повернення.

Якщо Catє єдиною твариною, Pooяка будь-якою особливою, або ми не хочемо, щоб тип Excrementбув головною визначальною ознакою a Cat, проміжний загальний клас може бути пропущений в ієрархії, що Catпоходить безпосередньо з AnimalWithSpecialisations, але якщо є - це кілька різних тварин, основною характеристикою яких є те, що вони Pooє якимось особливим чином, поділ "шаблону" на проміжні класи допомагає зберегтиCat клас досить чистим, хоча і ціною пари додаткових викликів віртуальних функцій.

Приклад коду показує, що більшість очікуваних операцій працюють "як очікувалося".

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 дає нам коваріантні типи повернення заміни. В основному: те, що ви хочете, просто працює .


-1

Ну, насправді можна повернути конкретний тип, який відрізняється від успадкованого типу повернення (навіть для статичних методів), завдяки 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);
    }
}

Я реалізував це в обгортці API, яку написав для своєї компанії. Якщо ви плануєте розробити API, це може покращити зручність використання та досвід розробників у деяких випадках використання. Тим не менше, використання dynamicмає вплив на продуктивність, тому намагайтеся уникати цього.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.