双重调度和替代方案

时间:2012-02-29 16:30:05

标签: c# .net double-dispatch

我试图找到一种更好的方法来处理一些不断增长的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) ...
}

我提出了两个选择:

Double Dispatch

我最近了解到访问者模式及其使用双重调度来处理这种情况。这很有吸引力,因为它会允许不需要的数据不传播(如果我们只想处理一个int,我们可以处理与DateTime不同的方式)。此外,处理不同类型的行为将局限于处理调度的单个类。但是,如果/必须支持新的值类型,则需要进行相当多的维护。

联盟班级

包含所支持的每种值类型的属性的类可以是每个类存储的内容。对值的任何操作都会影响相应的组件。这比双调度策略更简单,维护更少,但这意味着每一段数据都会不必要地传播,因为你不能再按照“我不对该数据类型进行操作”的方式进行区分。 ”。但是,如果/当需要支持新类型时,它们只需要进入这个类(加上需要创建的任何其他类来支持新数据类型)。

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

有更好的选择吗?这两个选项中的任何一个都有我认为不应该的东西吗?

4 个答案:

答案 0 :(得分:3)

方法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将使用@Juliet here提出的c#中的联合类型(替代实现here

答案 1 :(得分:0)

我告诉你我已经解决了类似的情况 - 将DateTime或TimeSpan的Ticks存储为集合中的double,并使用IComparable作为type参数的where约束。双精度/双精度转换由辅助类执行。

请参阅this previous question

有趣的是,这导致了其他问题,例如装箱和拆箱。我正在处理的应用程序需要极高的性能,所以我需要避免装箱。如果您能想到一般处理不同数据类型(包括DateTime)的好方法,那么我全都耳朵!

答案 2 :(得分:0)

为什么不实现您真正想要的接口,并允许实现类型定义值是什么?例如:

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

答案 3 :(得分:0)

好问题。我想到的第一件事是反思策略算法。无论用于保存引用的变量类型如何,运行时都可以静态或动态地告诉您最大派生类型的引用。但是,遗憾的是,它不会根据派生类型自动选择重载,只会选择变量类型。因此,我们需要在运行时询问真实类型是什么,并在此基础上,手动选择特定的重载。使用反射,我们可以动态构建一个标识为处理特定子类型的方法集合,然后查询其泛型类型的引用,并根据它在字典中查找实现。

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实现应该一次处理一个值。如果这不可行(假设您需要考虑特定类型的所有值),那么这变得非常简单;只需循环遍历Dictionary中的每个实现,向其传递完整的IEnumerable,并让这些实现使用OfType()Linq方法将列表过滤为特定的封闭泛型类型的对象。这将要求您运行列表中找到的所有ITimedValueEvaluator实现,如果列表中没有特定类型的项目,则会浪费精力。

这就是它的可扩展性;要支持ITimedValue的新通用关闭,只需添加相同类型的ITimedValueEvaluator的新实现。 Evaluator类将找到它,实例化一个副本并使用它。像大多数反射算法一样,它很慢,但实际反射部分是一次性交易。