Двойная отправка и альтернативы

Я пытаюсь найти лучший способ обработать некоторые растущие конструкции if для обработки классов разных типов. Эти классы, в конечном счете, обертывают вокруг разрозненных типов значений (int, DateTime и т.д.) С некоторой дополнительной информацией о состоянии. Таким образом, основное различие между этими классами - это тип данных, которые они содержат. Хотя они реализуют общие интерфейсы, их также нужно хранить в однородных коллекциях, поэтому они также реализуют не общий интерфейс. Экземпляры класса обрабатываются в соответствии с типом данных, которые они представляют, и их пропозиция продолжается или не продолжается на основе этого.

Хотя это не обязательно проблема .NET или С#, мой код находится на С#.

Примеры классов:

interface ITimedValue {
 TimeSpan TimeStamp { get; }
}

interface ITimedValue<T> : ITimedValue {
 T Value { get; }
}

class NumericValue : ITimedValue<float> {
 public TimeSpan TimeStamp { get; private set; }
 public float Value { get; private set; }
}

class DateTimeValue : ITimedValue<DateTime> {
 public TimeSpan TimeStamp { get; private set; }
 public DateTime Value { get; private set; }
}

class NumericEvaluator {
 public void Evaluate(IEnumerable<ITimedValue> values) ...
}

У меня есть два варианта:

Двойная отправка

Недавно я узнал о шаблоне посетителя и его использовании двойной отправки для обработки именно такого случая. Это требует, потому что это позволит нежелательным данным не распространяться (если мы хотим обрабатывать int, мы можем обрабатывать это иначе, чем DateTime). Кроме того, поведение того, как обрабатываются различные типы, ограничивается одним классом, который обрабатывает отправку. Но есть справедливое обслуживание, если/когда необходимо поддерживать новый тип значения.

Класс объединения

Класс, содержащий свойство для каждого поддерживаемого типа значений, может быть тем, что хранится в каждом из этих классов. Любая операция над значением повлияет на соответствующий компонент. Это менее сложное и меньшее обслуживание, чем стратегия двойного диспетчеризации, но это будет означать, что каждая часть данных будет распространяться без всякой необходимости, поскольку вы больше не можете различать строки "Я не работаю над этим типом данных". Однако, если/когда новые типы должны поддерживаться, им нужно только перейти в этот класс (плюс любые дополнительные классы, которые необходимо создать для поддержки нового типа данных).

class UnionData {
 public int NumericValue;
 public DateTime DateTimeValue;
}

Есть ли лучшие варианты? Есть ли что-то в любом из этих двух вариантов, которые я не считал, что должен?

Ответ 1

метод 1, используя динамический для двойной отправки (кредит идет http://blogs.msdn.com/b/curth/archive/2008/11/15/c-dynamic-and-multiple-dispatch.aspx). В принципе, шаблон вашего посетителя упрощается следующим образом:

class Evaluator {
 public void Evaluate(IEnumerable<ITimedValue> values) {
    foreach(var v in values)
    {
        Eval((dynamic)(v));
    }
 }

 private void Eval(DateTimeValue d) {
    Console.WriteLine(d.Value.ToString() + " is a datetime");
 }

 private void Eval(NumericValue f) {
    Console.WriteLine(f.Value.ToString() + " is a float");
 }

}

образец использования:

var l = new List<ITimedValue>(){
    new NumericValue(){Value= 5.1F}, 
    new DateTimeValue() {Value= DateTime.Now}};

new Evaluator()
    .Evaluate(l);
       // output:
       // 5,1 is a float
       // 29/02/2012 19:15:16 is a datetime

метод 2 будет использовать типы Union в С#, предложенные @Juliet здесь (альтернативная реализация здесь)

Ответ 2

Сообщаю, что я решил аналогичную ситуацию - это сохранить Ticks DateTime или TimeSpan как double в коллекции и с помощью IComparable в качестве ограничения на параметр типа. Преобразование в double/from double выполняется вспомогательным классом.

См. этот предыдущий вопрос.

Как ни странно, это приводит к другим проблемам, таким как бокс и распаковка. Приложение, над которым я работаю, требует чрезвычайно высокой производительности, поэтому мне нужно избегать бокса. Если вы можете придумать отличный способ для описания разных типов данных (включая DateTime), то я все уши!

Ответ 3

Почему бы просто не реализовать интерфейс, который вы действительно хотите, и позволить типу реализации определить, что такое значение? Например:

class NumericValue : ITimedValue<float> {
 public TimeSpan TimeStamp { get; private set; }
 public float Value { get; private set; }
}

class DateTimeValue : ITimedValue<DateTime>, ITimedValue<float> {
 public TimeSpan TimeStamp { get; private set; }
 public DateTime Value { get; private set; }
 public Float ITimedValue<Float>.Value { get { return 0; } }
}

class NumericEvaluator {
 public void Evaluate(IEnumerable<ITimedValue<float>> values) ...
}

Если вы хотите, чтобы поведение реализации DateTime изменялось в зависимости от конкретного использования (скажем, альтернативных реализаций функций Evaluate), тогда они по определению должны знать ITimedValue<DateTime>. Например, вы можете получить хорошее статически типизированное решение, предоставив, например, один или несколько делегатов Конвертер.

Наконец, если вы действительно хотите обрабатывать экземпляры NumericValue, просто отфильтруйте все, что не является экземпляром NumericValue:

class NumericEvaluator {
    public void Evaluate(IEnumerable<ITimedValue> values) {
        foreach (NumericValue value in values.OfType<NumericValue>()) {
            ....
        }
    }
}

Ответ 4

Хороший вопрос. Первое, что пришло мне в голову, было рефлексивным алгоритмом стратегии. Время выполнения может указывать, как статически, так и динамически, самый производный тип ссылки, независимо от типа переменной, которую вы используете для хранения ссылки. Однако, к сожалению, он не будет автоматически выбирать перегрузку на основе производного типа, а только тип переменной. Итак, во время выполнения мы должны спросить, что такое истинный тип, и на основе этого вручную выбрать конкретную перегрузку. Используя отражение, мы можем динамически строить коллекцию методов, идентифицированных как обработка определенного подтипа, а затем опросить ссылку для своего общего типа и искать реализацию в словаре на основе этого.
public interface ITimedValueEvaluator
{
   void Evaluate(ITimedValue value);
}

public interface ITimedValueEvaluator<T>:ITimedValueEvaluator
{
   void Evaluate(ITimedValue<T> value);
}

//each implementation is responsible for implementing both interfaces' methods,
//much like implementing IEnumerable<> requires implementing IEnumerable
class NumericEvaluator: ITimedValueEvaluator<int> ...

class DateTimeEvaluator: ITimedValueEvaluator<DateTime> ...

public class Evaluator
{
   private Dictionary<Type, ITimedValueEvaluator> Implementations;

   public Evaluator()
   {
      //find all implementations of ITimedValueEvaluator, instantiate one of each
      //and store in a Dictionary
      Implementations = (from t in Assembly.GetCurrentAssembly().GetTypes()
      where t.IsAssignableFrom(typeof(ITimedValueEvaluator<>)
      and !t.IsInterface
      select new KeyValuePair<Type, ITimedValueEvaluator>(t.GetGenericArguments()[0], (ITimedValueEvaluator)Activator.CreateInstance(t)))
      .ToDictionary(kvp=>kvp.Key, kvp=>kvp.Value);      
   }

   public void Evaluate(ITimedValue value)
   {
      //find the ITimedValue true type GTA, and look up the implementation
      var genType = value.GetType().GetGenericArguments()[0];

      //Since we're passing a reference to the base ITimedValue interface,
      //we will call the Evaluate overload from the base ITimedValueEvaluator interface,
      //and each implementation should cast value to the correct generic type.
      Implementations[genType].Evaluate(value);
   }   

   public void Evaluate(IEnumerable<ITimedValue> values)
   {
      foreach(var value in values) Evaluate(value);
   }
}

Обратите внимание, что главный Evaluator - единственный, который может обрабатывать IEnumerable; каждая реализация ITimedValueEvaluator должна обрабатывать значения по одному за раз. Если это невозможно (скажем, вам нужно учитывать все значения определенного типа), то это становится очень простым; просто перебирайте каждую реализацию в словаре, передавая ей полный IEnumerable, и эти реализации фильтруют список только для объектов определенного закрытого родового типа с использованием метода LinT. Это потребует, чтобы вы выполнили все реализации ITimedValueEvaluator, которые вы находите в списке, что является потраченным впустую, если в списке нет элементов определенного типа.

Красота этого - его расширяемость; для поддержки нового общего закрытия ITimedValue просто добавьте новую реализацию ITimedValueEvaluator того же типа. Класс Evaluator найдет его, создаст экземпляр и применит его. Как и большинство рефлексивных алгоритмов, он медленный, но фактическая отражающая часть является одноразовой сделкой.