双重调度和备选方案

本文关键字:方案 调度 | 更新日期: 2023-09-27 18:26:24

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

有更好的选择吗?这两个选项中是否有我没有考虑到的东西?

双重调度和备选方案

方法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#中的Union类型,正如@Juliet在这里提出的那样(这里的替代实现)

我告诉你我已经解决了类似的情况-通过在集合中将DateTime或TimeSpan的Ticks存储为double,并使用IComparable作为类型参数的where约束。从double到double的转换是由一个helper类执行的。

请参阅前面的问题。

有趣的是,这会导致其他问题,例如装箱和拆箱。我正在处理的应用程序要求极高的性能,所以我需要避免装箱。如果你能想出一个很好的方法来通用地处理不同的数据类型(包括DateTime),那么我就洗耳恭听了!

好问题。我首先想到的是一个反思性的策略算法。运行时可以静态或动态地告诉您最派生的引用类型,而不考虑用于保存引用的变量的类型。然而,不幸的是,它不会根据派生类型自动选择重载,只会根据变量类型。因此,我们需要在运行时询问真正的类型是什么,并在此基础上手动选择特定的重载。使用反射,我们可以动态地构建一个被识别为处理特定子类型的方法集合,然后询问引用的泛型类型,并在此基础上在字典中查找实现。

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类会找到它,实例化一个副本,然后使用它。就像大多数反射算法一样,它很慢,但实际的反射部分是一次性的。

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

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