问题 没有访客模式的动态调度


问题

我正在使用现有的库,我无法访问的源代码。该库代表AST。

我想复制此AST的一部分,但重命名过程中对变量的引用。由于可以有一个包含Expression对象的AssignCommand-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-function和运行时会找到最具体的一个。但是,情况并非如此,所有命令都会传递给 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;
        }
    }
}

8341
2017-12-27 22:33


起源

这听起来像你想要的东西 双重调度。 - Mike Bailey
这似乎正是我要找的东西的名字,谢谢。我会相应地标记帖子。但是,由于解决这个问题的常用方法似乎是使用实际类来实现访问者模式,因此这种方法是不可行的。正如我所说,我无法访问类的来源。我会尝试使用该关键字进行搜索。 - Alexander Weinert


答案:


与凯文的答案相似,我会考虑利用这个 dynamic 关键词。我只想提到另外两种方法。

现在,您并不需要访问源代码,只需要访问类型本身,即组件。只要类型是 public (不 private 要么 internal)这些应该工作:

动态访客

这个使用与传统方法类似的方法 游客 模式。

创建一个访问者对象,每个子类型有一个方法(结束类型,不是中间类或基类,如 Command),接收外部对象作为参数。

然后要调用它,在编译时你不知道确切类型的特定对象上,只需像这样执行访问者:

visitor.Visit((dynamic)target);

对于包含要访问的子表达式的类型,您还可以在访问者本身内处理递归。

处理程序字典

现在,如果您只想处理一些类型,而不是所有类型,那么创建一个类型可能更简单 Dictionary 处理程序,索引 Type。这样你可以检查字典是否有精确类型的处理程序,如果有,则调用它。通过标准调用可能会强制您在您的处理程序中进行强制转换,或者通过DLR调用,这将不会产生一点性能损失。


7
2017-12-31 17:45



谢谢你的解决方案。这实际上是单声道的,并给出了想要的行为。我是否认为这是对的 dynamic 至少在这种情况下,基本上表示“在运行时尽可能保持垂头丧气”? - Alexander Weinert
是的,这意味着将在运行时选择适当的重载,具体取决于“目标”的实际类型。 - Pablo Romeo


我不确定它是否在Mono中得到支持,但是你可以通过非常具体地使用泛型来完成你正在寻找的东西。 dynamicC#4.0中的关键字。你要做的是创建一个新的虚拟插槽,但语义略有不同(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;
    }
}

特别感兴趣的是类型 inputObj, @dynamic 在 Visit<T> 对于 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);
    }
}

我希望单声道是可行的!


4
2017-12-30 17:49



谢谢你的解决方案。这也适用于Mono(至少从版本2.10.8.1开始),但我选择了Pablo的第一个解决方案,因为它更简洁,不需要第二个功能。但是,你的回答告诉了我 dynamic-keyword,谢谢你:) - Alexander Weinert
我使用“双功能解决方案”的原因是因为您不必在每个呼叫位置都进行(动态)转换,这将极大地破坏您的代码。动态的所有代码膨胀都保留在泛型中,这是为所有类类型共享的。很高兴能帮到你 dynamic不过! - Kevin Frei
这是一个很好的答案。我更改了代码,使用了原始问题中使用的许多难以遵循的命名约定,以更好地说明这里发生的事情。 - Chris Marisic


这里是没有动态的版本,动态版本太慢(第一次调用):

public static class Visitor
{
    /// <summary>
    /// Create <see cref="IActionVisitor{TBase}"/>.
    /// </summary>
    /// <typeparam name="TBase">Base type.</typeparam>
    /// <returns>New instance of <see cref="IActionVisitor{TBase}"/>.</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,看看 更多细节


1
2018-05-06 20:56