没有访问者模式的动态派发

11

问题

我正在使用一个已经存在的库,但我无法访问其源代码。这个库代表了一个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>组成,其中AssignCommandCommand的子类。我以为只需将Command传递给Rename函数,运行时就会找到最具体的函数。然而,情况并非如此,所有命令都被传递到Command Rename(Command cmd, string prefix)。为什么会这样?有没有一种方法可以委托调用正确的函数而不使用丑陋的is操作?

最小示例

我已将此问题分解为以下NUnit测试代码:

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检查的情况下,以优雅、多态和面向对象的方式修复上述测试?”

扩展方法

我还尝试过使用扩展方法,代码如下。然而,这并没有解决问题,因为它们只是上述方法的语法糖:
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;
        }
    }
}

4
看起来你想要类似于“双重分发”的东西。 - Mike Bailey
那似乎正是我在寻找的东西的名称,谢谢。我会相应地标记这篇文章。然而,由于解决这个问题的常见方法似乎是使用实际类来实现访问者模式,所以这种方法不可行。正如我所说,我无法访问这些类的源代码。我会尝试使用该关键字进行搜索。 - Alexander Weinert
3个回答

7
与Kevin的答案类似,我会考虑利用dynamic关键字。我只提出两种额外的方法。
现在,您不真正需要访问源代码,只需要访问类型本身,也就是程序集。只要这些类型是public(而不是privateinternal),那么以下方法应该有效:
动态访问者
这个方法使用了与传统Visitor模式类似的方法。
创建一个访问者对象,每个子类型都有一个方法(末端类型,不是中间或基类比如Command),接收外部对象作为参数。
然后,要调用它,在编译时并不知道其确切类型的特定对象上,只需像这样执行访问者:
visitor.Visit((dynamic)target);

你还可以在访问器内部处理递归,用于那些具有子表达式需要访问的类型。

处理程序字典

现在,如果你只想处理几个类型而非全部类型,那么创建一个以Type为索引的处理程序Dictionary可能更简单。这样,你可以检查字典是否有针对该类型的处理程序,如果有,则调用它。可以通过标准调用进行(这可能强制你在处理程序内进行转换),也可以通过DLR调用进行(不需要转换,但会有一定的性能损失)。

感谢您的解决方案。它在mono中确实有效,并提供了所需的行为。我是否正确地看到,这个转换为dynamic基本上是在运行时“尽可能向下转换”,至少在这种情况下是这样吗? - Alexander Weinert
没错,这意味着在运行时会根据“target”的实际类型选择适当的重载。 - Pablo Romeo

4
我不确定在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关键字,非常感谢 :) - Alexander Weinert
2
我选择“2函数解决方案”的原因是因为这样你就不必在每个调用位置都进行(动态)转换,这将使你的代码膨胀得非常严重。所有动态部分的代码膨胀都留在通用部分中,这对于所有类类型都是共享的。不过很高兴能让你了解到dynamic - Kevin Frei
这是一个很好的答案。我将代码从原始问题中使用的许多难以理解的命名约定更改为更好地说明这里发生了什么。 - Chris Marisic

1

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

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,请查看更多细节


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接