没有访客模式的动态调度
Posted
技术标签:
【中文标题】没有访客模式的动态调度【英文标题】:Dynamic Dispatch without Visitor Pattern 【发布时间】:2012-12-13 06:50:24 【问题描述】:问题
我正在使用一个已经存在的库,我无权访问其源代码。这个库代表一个 AST。
我想复制此 AST 的一部分,但在此过程中重命名对变量的引用。由于可以有一个AssignCommand-Object,它包含一个Expression-object,我希望能够用它自己的函数复制每个对象,这样我就可以递归地调用它们。但是,由于我无权访问该库的代码,因此无法添加诸如CopyAndRename(string prefix)
之类的方法。
因此,我的方法是创建一个带有多个重载的函数Rename
。因此,我的家庭功能如下:
public static Command Rename(Command cmd, string prefix)
public static AssignCommand Rename(AssignCommand cmd, string prefix)
public static AdditionExpressionRename(AdditionExpression expr, string prefix)
....
函数现在由List<Command>
组成,其中AssignCommand
是Command
的子类。我假设我可以将Command
传递给Rename
函数,运行时会找到最具体的函数。但是,情况并非如此,所有命令都传递给Command Rename(Command cmd, string prefix)
。为什么会这样?有没有办法在不使用丑陋的is
-operations 的情况下将调用委托给正确的函数?
小例子
我已将此问题分解为以下 NUnit-Testcode
using NUnit.Framework;
public class TopClass
public int retVal;
public class SubClassA : TopClass
[TestFixture]
public class ThrowawayTest
private TopClass Foo (TopClass x)
x.retVal = 1;
return x;
private SubClassA Foo (SubClassA x)
x.retVal = 2;
return x;
[Test]
public void OverloadTest()
TopClass t = new TopClass();
TopClass t1 = new SubClassA();
SubClassA s1 = new SubClassA();
t = Foo (t);
t1 = Foo (t1);
s1 = Foo (s1);
Assert.AreEqual(1, t.retVal);
Assert.AreEqual(2, s1.retVal);
Assert.AreEqual(2, t1.retVal);
所以我的问题归结为:“如何在不诉诸is
-checks 的情况下以优雅、多态、面向对象的方式修复上述测试?”
扩展方法
我也尝试过使用如下扩展方法。这并没有解决问题,因为它们只是上述方法的语法糖:
using NUnit.Framework;
using ExtensionMethods;
public class TopClass
public int retVal;
public class SubClassA : TopClass
[TestFixture]
public class ThrowawayTest
private TopClass Foo (TopClass x)
x.retVal = 1;
return x;
private SubClassA Foo (SubClassA x)
x.retVal = 2;
return x;
[Test]
public void OverloadTest()
TopClass t = new TopClass();
TopClass t1 = new SubClassA();
SubClassA s1 = new SubClassA();
t.Foo(); s1.Foo(); t1.Foo();
Assert.AreEqual(1, t.retVal);
Assert.AreEqual(2, s1.retVal);
Assert.AreEqual(2, t1.retVal);
namespace ExtensionMethods
public static class Extensions
public static void Foo (this TopClass x)
x.retVal = 1;
public static void Foo (this SubClassA x)
x.retVal = 2;
【问题讨论】:
听起来你想要double dispatch这样的东西。 这似乎正是我要找的东西的名称,谢谢。我会相应地标记帖子。然而,由于解决这个问题的常用方法似乎是使用实际的类来实现访问者模式,所以这种方法是不可行的。正如我所说,我无权访问这些类的源代码。我会尝试使用该关键字进行搜索。 【参考方案1】:与凯文的回答类似,我会考虑利用 dynamic
关键字。我将仅提及另外两种方法。
现在,您实际上不需要访问源代码,您只需要访问类型本身,即程序集。只要类型是public
(不是private
或internal
),它们就可以工作:
动态访客
这个使用与传统Visitor 模式类似的方法。
创建一个访问者对象,每个子类型(最终类型,不是中间类或基类,如Command
)都有一个方法,接收外部对象作为参数。
然后调用它,在编译时你不知道确切类型的特定对象上,只需像这样执行访问者:
visitor.Visit((dynamic)target);
您还可以在访问者本身内处理递归,对于具有您想要访问的子表达式的类型。
处理程序字典
现在,如果您只想处理其中的几个类型,而不是所有类型,那么创建Dictionary
的处理程序可能会更简单,索引为Type
。这样你就可以检查字典是否有一个确切类型的处理程序,如果有,调用它。通过可能会强制您在处理程序中强制转换的标准调用,或者通过 DLR 调用,这不会但会产生一点性能影响)。
【讨论】:
感谢您的解决方案。这实际上在单声道中起作用并给出了想要的行为。至少在这种情况下,这种转换为dynamic
基本上表明“在运行时尽可能向下转换”,我是否认为正确?
对,就是说运行时会选择合适的重载,具体取决于“target”的实际类型。【参考方案2】:
我不确定 Mono 是否支持它,但您可以通过在 C# 4.0 中非常具体地使用泛型和 dynamic
关键字来完成您正在寻找的东西。您尝试做的是创建一个新的虚拟插槽,但语义略有不同(C# 虚拟函数不是协变的)。 dynamic
所做的是将函数重载决议推送到运行时,就像虚拟函数一样(尽管效率要低得多)。扩展方法和静态函数都具有编译时重载解决方案,因此变量的静态类型就是所使用的,这就是您要解决的问题。
public class FooBase
public int RetVal get; set;
public class Bar : FooBase
设置动态访问者。
public class RetValDynamicVisitor
public const int FooVal = 1;
public const int BarVal = 2;
public T Visit<T>(T inputObj) where T : class
// Force dynamic type of inputObj
dynamic @dynamic = inputObj;
// SetRetVal is now bound at runtime, not at compile time
return SetRetVal(@dynamic);
private FooBase SetRetVal(FooBase fooBase)
fooBase.RetVal = FooVal;
return fooBase;
private Bar SetRetVal(Bar bar)
bar.RetVal = BarVal;
return bar;
特别感兴趣的是Visit<T>
中inputObj, @dynamic
的类型Visit(new Bar())
。
public class RetValDynamicVisitorTests
private readonly RetValDynamicVisitor _sut = new RetValDynamicVisitor();
[Fact]
public void VisitTest()
FooBase fooBase = _sut.Visit(new FooBase());
FooBase barAsFooBase = _sut.Visit(new Bar() as FooBase);
Bar bar = _sut.Visit(new Bar());
Assert.Equal(RetValDynamicVisitor.FooVal, fooBase.RetVal);
Assert.Equal(RetValDynamicVisitor.BarVal, barAsFooBase.RetVal);
Assert.Equal(RetValDynamicVisitor.BarVal, bar.RetVal);
我希望这在 Mono 中是可行的!
【讨论】:
感谢您的解决方案。这也适用于 Mono(至少从 2.10.8.1 版开始),但我选择了 Pablo 的第一个解决方案,因为它更简洁并且不需要第二个函数。但是,您的回答向我显示了dynamic
-keyword,谢谢:)
我选择“2 功能解决方案”的原因是,您不必在每个调用位置都进行(动态)强制转换,这会使您的代码非常臃肿。动态的所有代码膨胀都保留在泛型中,为所有类类型共享。不过,很高兴您能找到dynamic
!
这是一个很好的答案。我更改了代码,不再使用原始问题中使用的许多难以遵循的命名约定,以更好地说明这里发生的事情。【参考方案3】:
这里是没有动态的版本,动态版本太慢(第一次调用):
public static class Visitor
/// <summary>
/// Create <see cref="IActionVisitorTBase"/>.
/// </summary>
/// <typeparam name="TBase">Base type.</typeparam>
/// <returns>New instance of <see cref="IActionVisitorTBase"/>.</returns>
public static IActionVisitor<TBase> For<TBase>()
where TBase : class
return new ActionVisitor<TBase>();
private sealed class ActionVisitor<TBase> : IActionVisitor<TBase>
where TBase : class
private readonly Dictionary<Type, Action<TBase>> _repository =
new Dictionary<Type, Action<TBase>>();
public void Register<T>(Action<T> action)
where T : TBase
_repository[typeof(T)] = x => action((T)x);
public void Visit<T>(T value)
where T : TBase
Action<TBase> action = _repository[value.GetType()];
action(value);
接口声明:
public interface IActionVisitor<in TBase>
where TBase : class
void Register<T>(Action<T> action)
where T : TBase;
void Visit<T>(T value)
where T : TBase;
用法:
IActionVisitor<Letter> visitor = Visitor.For<Letter>();
visitor.Register<A>(x => Console.WriteLine(x.GetType().Name));
visitor.Register<B>(x => Console.WriteLine(x.GetType().Name));
Letter a = new A();
Letter b = new B();
visitor.Visit(a);
visitor.Visit(b);
控制台输出:A,B,看看more details。
【讨论】:
以上是关于没有访客模式的动态调度的主要内容,如果未能解决你的问题,请参考以下文章
Spring4+Springmvc+quartz实现多线程动态定时调度