Возвращает один из двух возможных объектов разных типов, использующих метод

У меня есть 2 класса:

public class Articles
{
    private string name;

    public Articles(string name)
    {
        this.name = name;
    }

    public void Output()
    {
        Console.WriteLine("The class is: " + this.GetType());
        Console.WriteLine("The name is: " + name);
    }
}

А также

public class Questionnaire 
{
    private string name;

    public Questionnaire(string name)
    {
        this.name = name;
    }

    public void Output()
    {
        Console.WriteLine("The class is: " + this.GetType());
        Console.WriteLine("The name is: " + name);
    }
}

Я хочу написать метод, который принимает целое число (1 означает, что должно быть возвращено Articles, 2 означает Questionnaire) и имя.

Этот метод должен возвращать экземпляр одного из этих двух классов:

public [What type??] Choose(int x, string name)
    {
        if (x == 1)
        {
           Articles art = new Articles(name);
           return art;
        }
        if (x == 2)
        {
            Questionnaire ques = new Questionnaire(name);
            return ques;
        }
    }

Какой тип возврата следует использовать, чтобы я мог вызвать Output() для результата?


person Sashko Chehotsky    schedule 05.07.2013    source источник
comment
Если вы считаете, что оба типа имеют (много) общего, вы должны позволить им обоим наследоваться от одного и того же базового класса или, по крайней мере, реализовать один и тот же интерфейс.   -  person Tim Schmelter    schedule 05.07.2013
comment
Обычно это называется утиным вводом — определенно возможно с C# (см. dynamic), но рассмотрите возможность использования строго типизированных решений, показанных в ответах.   -  person Alexei Levenkov    schedule 03.10.2014


Ответы (6)


Почему бы не использовать базовый класс, который определено Output. Затем верните базу.

public abstract class BaseType {
    public abstract void Output();
}

И Articles, и Questionaire должны наследовать этот BaseType.

public class Articles : BaseType {
  // Output method here
}

public class Questionaire : BaseType {
 // Output method here
}

Затем вы можете сделать:

public static BaseType Choose(int x, string name) 
{
    if (x == 1)
    {
       Articles art = new Articles(name);
       return art;
    }
    if (x == 2)
    {
        Questionnaire ques = new Questionnaire(name);
        return ques;
    }
}

Вы также можете добиться этого с помощью interface.

public interface IInterface {
    void Output();
}

public class Articles : IInterface {
  // Output method here
}

public class Questionaire : IInterface {
 // Output method here
}

Затем вам придется изменить метод Choose, чтобы он возвращал IInterface, а не BaseType. Какой бы вы ни выбрали, зависит от вас.

Примечание: даже если вы не можете изменить исходные классы, вы все равно можете использовать эти подходы, прежде чем прибегать к dynamic, предоставляя классы-оболочки, которые реализуют интерфейс и либо наследуют исходные, либо перенаправляют вызовы соответствующему методу:

public class ArticlesProxy : Articles, IInterface 
{
  public ArticlesProxy(string name) : base(name){}

}

public class QuestionaireProxy : Questionaire, IInterface {
  Questionaire inner;
  public QuestionaireProxy(string name) {  inner = new Questionaire(name); }

  public void Output() { inner.Output();}

}
person Darren    schedule 05.07.2013
comment
Хорошей практикой является либо использование базового типа, такого как класс Printable, для примера, либо интерфейс, такой как IPrintable. - person marcelo-ferraz; 05.07.2013
comment
Имея в виду, что я использовал это имя, чтобы оно относилось к вашим классам - person marcelo-ferraz; 05.07.2013
comment
Это более или менее вариация шаблона проектирования Factory: oodesign.com/factory-pattern.html - person Amish Programmer; 06.07.2013

Как насчет такого:

public interface IHasOutput
{
    void Output();
}

public class Articles : IHasOutput

public class Questionnaire : IHasOutput

а потом:

public static IHasOutput Choose...

Конечно, вы можете назвать свой интерфейс как угодно, кроме IHasOutput, я просто не знаю, как его назвать. Вот для чего нужны интерфейсы. Две разные конкретные реализации, которые имеют общий интерфейс. Теперь, когда вы вызываете его, вы можете сделать это:

var entity = MyClass.Choose(1, "MyName");
entity.Output();

и не имеет значения, какая конкретная реализация возвращается. Вы знаете, что он реализует общий интерфейс.

person Mike Perrenoud    schedule 05.07.2013
comment
Вы должны переименовать IEntity в IHasOutput, потому что это представляет очень специфическое поведение, а не общий дескриптор всего класса. Но это все еще очень правильный ответ. - person Steve B; 05.07.2013

Ответы, представленные здесь, великолепны, но мне не нравится параметр x, который выбирает, какой тип должен быть создан. Это приводит к использованию магического числа, которое может стать головной болью даже для вас позже.

Здесь вы можете воспользоваться дженериками, т.е. сделать метод Choose:

public static T Choose<T>(string name)
        // type constraint to ensure hierarchy.
        where T : BaseClass // BaseClass have common functionality of both class.
    {
        // Unfortunately you can't create instance with generic and pass arguments
        // to ctor. So you have to use Activator here.
        return (T)Activator.CreateInstance(typeof(T), new[] { name });
    }

Применение:

Articles article = ClassWithChooseMethod.Choose<Articles>("name");
Questionnaire questionnaire = ClassWithChooseMethod.Choose<Questionnaire>("name2");

Демо

Изменить

Как упоминал @OlivierJacot-Descombes в комментарии x, выбор типа может быть введен пользователем. В этом случае вы можете создать enum с соответствующими значениями:

enum ArticleType {
    Articles = 1,
    Questionnaire = 2
}

И иметь перегрузку Choose:

public static BaseClass Choose(ArticleType type, string name) {
    switch (type) {
        case ArticleType.Articles:
            return ClassWithChooseMethod.Choose<Articles>(name);
        case ArticleType.Questionnaire:
            return ClassWithChooseMethod.Choose<Questionnaire>(name);
        default:
            return default(BaseClass);
    }
}

и использование:

var obj = ClassWithChooseMethod.Choose((ArticleType)userInput, "some name");

Это дает вам возможность сохранить ваш код более чистым и полезным для дальнейшего обслуживания (например, вы можете изменить логику создания класса в Choose).

P.S. Возможно, вам будет интересно узнать больше о фабричном шаблоне.

person Leri    schedule 05.07.2013
comment
А откуда у вас тип T? Скорее всего, значение x не является жестко закодированным, а исходит от пользовательского ввода. - person Olivier Jacot-Descombes; 05.07.2013
comment
@OlivierJacot-Descombes Может быть, это так, а может и нет. В ОП об этом не упоминалось. И даже если это так, я бы создал enum для обработки типов и использовал бы switch вне метода Choose, или у меня была бы его неуниверсальная перегрузка, которая внутренне использовала бы универсальный метод для возврата соответствующего типа. - person Leri; 05.07.2013

Если они не используют один и тот же базовый класс или интерфейс, вы в значительной степени застряли либо с object, либо с dynamic.

person John Kraft    schedule 05.07.2013
comment
Это действительно никогда не требуется - это просто плохой дизайн кода - person TGlatzer; 05.07.2013
comment
Я не говорил, что это хороший дизайн. Только то, что с дизайном, как заявлено, это варианты. - person John Kraft; 05.07.2013

Самый гибкий способ решить эту проблему — написать интерфейс, а также реализующий его абстрактный базовый класс. Таким образом, у вас есть свобода наследовать класс от базового класса или реализовать интерфейс напрямую, если базовый класс не удовлетворяет вашим потребностям в особом случае или если класс уже является производным от другого класса. Также сделайте метод Output виртуальным; это позволяет вам переопределить его, если это необходимо. Также сделайте name защищенным; это позволяет вам использовать его в производных классах

public interface IHasOutput
{
    void Output();
}

public abstract class OutputBase : IHasOutput
{
    protected string _name;

    public OutputBase(string name)
    {
        _name = name;
    }

    #region IHasOutput Members

    public virtual void Output()
    {
        Console.WriteLine("The class is: " + this.GetType());
        Console.WriteLine("The name is: " + _name);
    }

    #endregion

    public static IHasOutput Choose(int x, string name)
    {
        switch (x) {
            case 1:
                return new Articles(name);
            case 2:
                return new Questionnaire(name);
            default:
                return null;
        }
    }
}

public class Articles : OutputBase
{
    public Articles(string name)
        : base(name)
    {
    }
}

public class Questionnaire : OutputBase
{
    public Questionnaire(string name)
        : base(name)
    {
    }
}

ОБНОВЛЕНИЕ

Еще один очень простой способ решить проблему — переопределить ToString:

public override string ToString()
{
    return String.Format("The class is: {0}\r\nThe name is: {1}", 
                         this.GetType(), _name);
}

Вы бы назвали это так:

object obj = Factory.Choose(1, "Test");
Console.WriteLine(obj);

Не требуется интерфейс и базовый класс! Ну, если быть точным, базовый класс object конечно же.

person Olivier Jacot-Descombes    schedule 05.07.2013

У вас есть 3 варианта:

1) Сделайте Анкету и Статью наследниками одного и того же базового класса и сделайте тип этого базового класса возвращаемым типом вашего метода.

2) Сделайте свой возвращаемый тип объекта.

3) Сделайте тип возвращаемого значения динамическим.

person Bill Gregg    schedule 05.07.2013