Динамическая отправка без шаблона посетителя

Проблема

Я работаю с уже существующей библиотекой, исходный код которой у меня отсутствует. Эта библиотека представляет AST.

Я хочу скопировать части этого АСТ, но переименуйте ссылки на переменные в этом процессе. Поскольку может существовать AssignCommand-Object, который содержит объект Expression, я хочу иметь возможность копировать каждый объект со своей собственной функцией, поэтому я могу называть их рекурсивно. Однако, поскольку у меня нет доступа к коду библиотеки, я не могу добавить такой метод, как 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)
....

Теперь функция состоит из a List<Command>, где AssignCommand является подклассом Command. Я предположил, что могу просто передать Command в Rename -функцию, и среда выполнения найдет наиболее конкретную. Однако это не так, и все команды передаются в Command Rename(Command cmd, string prefix). Почему это так? Есть ли способ делегировать вызов правильной функции без использования уродливых is -операций?

Минимальный пример

Я нарушил эту проблему до следующего 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;
        }
    }
}

Ответ 1

Как и в случае с ответом Кевина, я бы предпочел использовать ключевое слово dynamic. Я просто упомянул о двух дополнительных подходах.

Теперь вам не нужен доступ к исходному коду, вам просто нужен доступ к самим типам, то есть к сборке. Пока типы public (не private или internal), они должны работать:

Динамический посетитель

Этот подход использует аналогичный подход к обычному шаблону Visitor.

Создайте объект-посетитель с одним методом для каждого подтипа (конечные типы, а не промежуточные или базовые классы, такие как Command), получив внешний объект как параметр.

Затем, чтобы вызвать его, на конкретном объекте, о котором вы не знаете точного типа во время компиляции, просто выполните посетителя следующим образом:

visitor.Visit((dynamic)target);

Вы также можете обрабатывать рекурсию внутри самого посетителя, для типов, у которых есть подвыражения, которые вы хотите посетить.

Словарь обработчиков

Теперь, если вы хотите обрабатывать несколько типов, не все из них, вам может быть проще просто создать Dictionary обработчиков, индексированных с помощью Type. Таким образом, вы можете проверить, есть ли у словаря обработчик точного типа, если он это делает, вызывать его. Либо через стандартный вызов, который может заставлять вас использовать в вашем обработчике, либо через вызов DLR, который не будет, но будет иметь небольшой успех).

Ответ 2

Я не уверен, поддерживается ли оно в Mono, но вы можете выполнить то, что ищете, очень специфичным использованием генериков и ключевым словом dynamic в С# 4.0. То, что вы пытаетесь сделать, это создать новый виртуальный слот, но с немного другой семантикой (виртуальные функции С# не ковариантны). То, что 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);
    }
}

Я надеюсь, что это возможно в Mono!

Ответ 3

Здесь версия без динамической, динамической версии слишком медленная (первый вызов):

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, посмотрите подробнее.