Динамическая отправка без шаблона посетителя
проблема
Я работаю с уже существующей библиотекой, к исходному коду которой у меня нет доступа. Эта библиотека представляет собой AST.
Я хочу скопировать части этого AST, но переименовать ссылки на переменные в процессе. Поскольку может существовать объект AssignCommand, который содержит объект 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)
....
Функция теперь состоит из 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
-Проверяет?"
Методы расширения
Я также попытался использовать методы расширения следующим образом. Это не решило проблему, так как они являются просто синтаксическим сахаром для подхода выше:
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;
}
}
}
3 ответа
Как и в случае с ответом Кевина, я хотел бы воспользоваться dynamic
ключевое слово. Я просто упомяну два дополнительных подхода.
Теперь вам не нужен доступ к исходному коду, вам просто нужен доступ к самим типам, то есть к сборке. Пока типы public
(не private
или же internal
) это должно работать:
Динамический посетитель
Этот использует аналогичный подход к обычному шаблону посетителя.
Создайте объект посетителя с одним методом для каждого подтипа (конечные типы, а не промежуточные или базовые классы, такие как Command
), получая внешний объект в качестве параметра.
Затем, чтобы вызвать его для определенного объекта, точный тип которого вы не знаете во время компиляции, просто выполните посетитель следующим образом:
visitor.Visit((dynamic)target);
Вы также можете обрабатывать рекурсию внутри самого посетителя для типов, у которых есть подвыражения, которые вы хотите посетить.
Словарь обработчиков
Теперь, если вы хотите работать только с несколькими типами, а не со всеми, вам может быть проще создать Dictionary
обработчиков, проиндексированных Type
, Таким образом, вы можете проверить, есть ли в словаре обработчик для точного типа, если он есть, вызвать его. Либо с помощью стандартного вызова, который может заставить вас выполнить приведение в вашем обработчике, либо с помощью вызова DLR, который не будет, но будет иметь некоторое снижение производительности).
Я не уверен, поддерживается ли он в Mono, но вы можете добиться того, что вы ищете, с помощью очень специфического использования обобщений и dynamic
Ключевое слово в C# 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);
}
}
Я надеюсь, что это возможно в Mono!
Вот версия без динамической, динамическая версия слишком медленная (первый вызов):
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, посмотрите подробности.