Как правильно работать с вызывающими методами в связанных, но разных классах в С#

Честно говоря, я не был уверен, как сформулировать этот вопрос, так что простите меня, если фактический вопрос - это не то, что вы ожидали от названия. С# - это первый статически типизированный язык, который я когда-либо программировал, и этот аспект этого был для меня до сих пор абсолютной головной болью. Я уверен, что у меня просто нет хорошей справки по основным идеям, связанным с тем, как создать систему в статически типизированной манере.

Вот приблизительное представление о том, что я пытаюсь сделать. Предположим, что у меня есть иерархия классов:

abstract class DataMold<T>
{
    public abstract T Result { get; }
}

class TextMold : DataMold<string>
{
  public string Result => "ABC";
}  

class NumberMold : DataMold<int>
{
   public int Result => 123
}

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

List<DataMold<T>> molds = new List<DataMold<T>>();
molds.Add(new TextMold());
molds.Add(new NumberMold());

foreach (DataMold<T> mold in molds)
    Console.WriteLine(mold.Result);

Как вы, наверное, уже знаете, это не работает. Из того, что я читал в своих поисках, это связано с тем, что я не могу объявить список типа DataMold<T>. Каков правильный способ сделать что-то подобное?

Ответы

Ответ 1

Короткий ответ: вы не можете.

Одна из вещей, которые несовместимы с родовыми типами, заключается в том, что они не связаны друг с другом. List<int>, например, не имеет никакого отношения к List<string>. Они не наследуют друг от друга, и вы не можете отбрасывать друг друга.

Вы можете объявить ковариационную связь, которая очень похожа на отношение наследования, но не между int и string как вы заявили, поскольку один из них является типом значений, а один - ссылочным.

Ваша единственная альтернатива - добавить другой интерфейс, который у них есть, например:

interface IDataMold
{
}

abstract class DataMold<T> : IDataMold
{
    public abstract T Result { get; }
}

Теперь вы можете сохранить все ваши формы в List<IDataMold>. Однако интерфейс не имеет свойств, поэтому у вас будет время heckuva получить что-нибудь из этого. Вы можете добавить некоторые свойства, но они не будут специфичны для типов, так как IDataMold не имеет параметра общего типа. Но вы можете добавить общее свойство

interface IDataMold
{
    string ResultString { get; }
}

... и реализовать его:

abstract class DataMold<T>
{
    public abstract T Result { get; }
    public string ResultString => Result.ToString();
}

Но если вам нужно только отображать эквивалент строки для каждого элемента, вы можете просто переопределить ToString():

class TextMold : DataMold<string>
{
    public string Result => "ABC";
    public override string ToString() => Result.ToString();
}

Теперь вы можете сделать это:

List<IDataMold> molds = new List<IDataMold>();
molds.Add(new TextMold());
molds.Add(new NumberMold());

foreach (var mold in molds)
{
    Console.WriteLine(mold.ToString());
}

Ответ 2

Вы ищете ковариацию. См. Ключевое слово out перед параметром типа T:

// Covariance and contravariance are only possible for
// interface and delegate generic params
public interface IDataMold<out T> 
{
   T Result { get; }
}

abstract class DataMold<T> : IDataMold<T>
{
  public abstract T Result { get; }
}

class StringMold : DataMold<string> {}

class Whatever {}

class WhateverMold : DataMold<Whatever> {}

Теперь наследуем DataMold<T> и создаем List<IDataMold<object>>:

var molds = new List<IDataMold<object>>();
molds.Add(new StringMold());
molds.Add(new WhateverMold());

BTW, вы не можете использовать ковариацию, когда дело доходит до того, чтобы IDataMold<int> к IDataMold<object>. Вместо того, чтобы повторять то, что уже было объяснено, см. Другие вопросы и ответы: почему ковариация и контравариантность не поддерживают тип ценности

Если вы действительно вынуждены реализовать IDataMold<int>, этот список может иметь object типа:

var molds = new List<object>();
molds.add(new TextMold());
molds.add(new NumberMold());

И вы можете использовать Enumerable.OfType<T> для получения подмножеств molds:

var numberMolds = molds.OfType<IDataMold<int>>();
var textMolds = molds.OfType<IDataMold<string>>();

Кроме того, вы можете создать два списка:

var numberMolds = new List<IDataMold<int>>();
var textMolds = new List<IDataMold<string>>();

Поэтому вы можете смешать их позже как IEnumerable<object> если вам нужно:

var allMolds = numberMolds.Cast<object>().Union(textMolds.Cast<object>());

Ответ 3

Вы можете использовать шаблон посетителя:

Добавьте интерфейс посетителя, который принимает все ваши типы, и внедрите посетителя, который выполняет действие, которое вы хотите применить ко всем DataMolds:

interface IDataMoldVisitor
{  void visit(DataMold<string> dataMold);
   void visit(DataMold<int> dataMold);
}

// Console.WriteLine for all
class DataMoldConsoleWriter : IDataMoldVisitor
{  public void visit(DataMold<string> dataMold)
   {  Console.WriteLine(dataMold.Result);
   }
   public void visit(DataMold<int> dataMold)
   {  Console.WriteLine(dataMold.Result);
   }
}

Добавьте интерфейс-акцептор, который может содержать ваш список, и его классы DataMold реализуют:

interface IDataMoldAcceptor
{  void accept(IDataMoldVisitor visitor);
}

abstract class DataMold<T> : IDataMoldAcceptor
{  public abstract T Result { get; }
   public abstract void accept(IDataMoldVisitor visitor);
}

class TextMold : DataMold<string>
{  public string Result => "ABC";
   public override void accept(IDataMoldVisitor visitor)
   {  visitor.visit(this);
   }
}  

class NumberMold : DataMold<int>
{  public int Result => 123;
   public override void accept(IDataMoldVisitor visitor)
   {  visitor.visit(this);
   }
}

И, наконец, выполните его с:

// List now holds acceptors
List<IDataMoldAcceptor> molds = new List<IDataMoldAcceptor>();
molds.Add(new TextMold());
molds.Add(new NumberMold());

// Construct the action you want to perform
DataMoldConsoleWriter consoleWriter = new DataMoldConsoleWriter();
// ..and execute for each mold
foreach (IDataMoldAcceptor mold in molds)
    mold.accept(consoleWriter);

Выход:

ABC
123

Ответ 4

dynamic

Это можно сделать с помощью dynamic ключевого слова за счет производительности и безопасности типов.

var molds = new List<object>();     // System.Object is the most derived common base type.
molds.Add(new TextMold());
molds.Add(new NumberMold());

foreach (dynamic mold in molds)
    Console.WriteLine(mold.Result);

Теперь, когда mold dynamic, С# проверяет тип mold во время выполнения, а затем выясняет, что из .Result означает.