Двойная отправка и альтернативы - PullRequest
11 голосов
/ 29 февраля 2012

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

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

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

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) ...
}

Я предложил два варианта:

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

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

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

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

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

Есть ли лучшеопции?Есть ли что-то в этих двух вариантах, которые я не учел?

Ответы [ 4 ]

3 голосов
/ 29 февраля 2012

метод 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 будет использовать типы объединения в c #, как предложено @Juliet здесь (альтернативная реализация здесь )

0 голосов
/ 29 февраля 2012

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

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>. Вы можете получить хорошее статическое решение, предоставив, например, одного или нескольких Converter делегатов.

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

class NumericEvaluator {
    public void Evaluate(IEnumerable<ITimedValue> values) {
        foreach (NumericValue value in values.OfType<NumericValue>()) {
            ....
        }
    }
}
0 голосов
/ 29 февраля 2012

Хороший вопрос.Первое, что пришло мне в голову, это алгоритм отражающей стратегии.Среда выполнения может сказать вам, статически или динамически, наиболее производный тип ссылки, независимо от типа переменной, которую вы используете для хранения ссылки.Однако, к сожалению, он не будет автоматически выбирать перегрузку на основе производного типа, а только тип переменной.Итак, нам нужно спросить во время выполнения, что является истинным типом, и, исходя из этого, вручную выбрать конкретную перегрузку.Используя рефлексию, мы можем динамически создать коллекцию методов, идентифицированных как обрабатывающие определенный подтип, затем запросить ссылку для ее универсального типа и найти реализацию в словаре на основе этого.

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's true type's 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 OfType ().Это потребует от вас запуска всех реализаций ITimedValueEvaluator, которые вы найдете в списке, что является бесполезным усилием, если в списке нет элементов определенного типа.

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

0 голосов
/ 29 февраля 2012

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

Пожалуйста, посмотрите этот предыдущий вопрос .

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

...