提问人:Alberto 提问时间:3/20/2023 最后编辑:Alberto 更新时间:3/20/2023 访问量:65
C# - 不能对派生泛型类型的数组使用双重调度访客
C# - Can't use double-dispatch Visitor for array of derived generic types
问:
我想知道为什么我无法在 C# 中运行泛型对象的任何 Visitor。
出于某种原因,程序总是选择我的泛型中最不具体的泛型重载。
P.S.:我不是在寻求替代方法的建议(比如不使用访客)。
我怀疑问题是我为我的访问者使用泛型而不是非泛型可访问类型,但不明白为什么(因为泛型的重载应该起作用,特别是考虑到具体类型 TimedValue 的特定实例,其中 T 是 int 或其他例如,是将调用分派给访问者的实例)。
我有一个对性能敏感的问题,我必须根据传入的对象类型(in )处理几种可能的对象类型,这些对象在许多地方派生自通用层次结构(例如),但我不想在整个代码中使用开关或if-elses(设计选择),我也不想使用(出于性能问题)。ITimedValue
T
TimedValue<T>
dynamic
我首先通过添加时间戳来标记该层次结构下的对象,然后进行大量处理,这些对象被传递到许多不同的地方,包括反应式流等。
我认为具有双重调度的访客模式应该可以处理这个问题。但是,泛型在捉弄我,无论我做什么,我都无法让它们调用我的特定重载。
精简代码以举例说明该问题
这是有问题的代码(非常精简到最小的可重现示例)。
基类型和泛型派生类型有一个接口。ITimedValue
TimedValue<T>
然后,这些类型的访问者的接口,以及一个非常精简的访问者实现(现在甚至没有积累状态),只是为了表明问题完全在于调用适当的访问者和重载。ITimedValueVisitor
TimedValue<T>
DemoValueVisitor
Visit
VisitSpecific
我使用了不同的名称,以便更明显地将具体类型称为具体类型。VisitSpecific
Visit
我还尝试将模板化和非模板化重载添加到该方法中,以处理特定的派生类型,如 等,并且两者都具有相同的行为 - 只是被调用。TimedValue<bool>
VisitSpecific<T>(TimedValue<T> typedVariant)
到目前为止,唯一有效的方法是摆脱双重调度并通过开关应用强制转换,因此它会在强制转换之后立即调用特定,但这根本不需要(此时这不再真正使用双重调度)。Visit
VisitSpecific
public interface ITimedValue // Base type (we'll have arrays and streams of these)
{
public void Accept(ITimedValueVisitor visitor);
};
/**
* I really wanted to use the generic (instead of having a specific type, per each `T` I want to support).
* I'd rather avoid non-generic types -- e.g. TimedValueBool instead of TimedValue<bool> -- though I suspect this is the source of all trouble.
*/
public class TimedValue<T> : ITimedValue // Concrete types passed on streams
{
// Some other special data common to all subtypes...
public long Time { get; private set; }
public T Value { get; private set; }
public TimedValue(T value)
{
Value = value;
}
public void Accept(ITimedValueVisitor visitor)
{
visitor.VisitSpecific<T>(this); // Also removed this <T>, it's the same.
}
public override string ToString()
{
return $"TimedValue<{typeof(T)}>({Time}, {Value})";
}
}
public interface ITimedValueVisitor // A visitor interface
{
void Visit(ITimedValue typedVariant);
void VisitSpecific<T>(TimedValue<T> typedVariant);
void VisitSpecific<T>(TimedValue<bool> typedVariant);
void VisitSpecific<T>(TimedValue<string> typedVariant);
// and many others... (these above are just to make the example simpler)
}
public class DemoTimedVisitor : ITimedValueVisitor // Some dummy visitor to show the problem
{
private ITestOutputHelper logger;
public DemoTimedVisitor(ITestOutputHelper logger)
{
this.logger = logger;
}
// Do I need this at all? It's so I don't get errors like "can't cast `ITimedValue` to `TimedValue<T>`".
public void Visit(ITimedValue timedValue)
{
timedValue.Accept(this);
}
/* I'm forced to have this by the type system. However, the system only calls this
* independently of the specific type, and if we have more specific overloads.
*/
public void VisitSpecific<T>(TimedValue<T> typedVariant)
{
logger.WriteLine("Called Visitor GENERIC overload!!!!" + typedVariant.ToString());
}
// I also tried without success having VisitSpecific(TimedValue<bool> typedVariant) instead. Never gets called :(
public void VisitSpecific<T>(TimedValue<bool> typedVariant)
{
logger.WriteLine("Called Visitor BOOL overload! " + typedVariant.ToString());
}
// Ditto.
public void VisitSpecific<T>(TimedValue<string> typedVariant)
{
logger.WriteLine("Called Visitor STRING overload!" + typedVariant.ToString());
}
}
一些测试代码(使用 XUnit)举例说明了这个问题:
using Xunit.Abstractions;
public class TestDemo
{
readonly ITestOutputHelper logger;
public TestDemo(ITestOutputHelper logger)
{
this.logger = logger;
}
[Fact]
public void DemoOfTargetAPIUsage() // Just to show roughly the point.
{
DemoTimedVisitor visitor = new DemoTimedVisitor(logger); // Ideally it would be collecting data in it (but in this question it does nothing for simplicity).
// Ideally we'd be using:
List<ITimedValue> timedValues = new List() { new TimedValue<...>(...), ... }
// Followed by:
foreach (var timedValue in timedValues)
{
visitor.Visit(timedValue);
}
visitor.GetState()... // What we'd want
}
[Fact]
public void EvenBasicStuffFails() // most basic example.
{
DemoTimedVisitor visitor = new DemoTimedVisitor(logger); // Ideally it would be collecting data in it (but in this question it does nothing for simplicity).
ITimedValue vbool = new TimedValue<bool>(true);
ITimedValue vstring = new TimedValue<string>("foo");
ITimedValue vfloat = new TimedValue<float>(9999.999F);
vbool.Accept(visitor); // uses unspecific overload :(
vstring.Accept(visitor); // uses unspecific overload :(
vfloat.Accept(visitor); // uses unspecific overload as it should (no specific visitor overloads defined) :+1:
...
}
}
我不太习惯 C# 的泛型,显然我一定在做一些非常愚蠢的事情,我只是不明白为什么 Accept 总是调用 .它是编译时唯一可用的类型吗?但是在内部,实现应该因类型而异——或者至少这是我想要的:(VisitSpecific<T>(TimedValue<T>)
Accept
T
我是否需要删除泛型,例如 -> 或者我是否在此双重调度中遗漏了某些内容?(我宁愿不要)TimedValue<Custom>
TimedValueCustom
谢谢!
答:
visitor.VisitSpecific<T>(this);
在这一行中,编译器只知道 是 ,因此在所有情况下都会调用的最佳匹配。C# 编译器将在生成的 IL 中对此方法句柄进行硬编码。尽管如果方法足够“热”,运行时可以对调用站点进行 JIT 专用化以对调用站点进行非虚拟化。this
TimedValue<T>
void VisitSpecific<T>(TimedValue<T> typedVariant)
您需要在编译器已知参数类型的某处编写自己的代码,以便在编译时解析正确的方法。也许是开关表达式;case TimedValue<bool> boolValue:
或者,您可以跳过泛型并定义每个 .TimedValueBool : ITimedValue
或者,您可以使用在运行时选择方法,为此付出性能代价。dynamic
评论
void VisitSpecific<T>(TimedValue<T> typedVariant)
void VisitSpecific<T>(TimedValue<int> typedVariant)
void VisitSpecific<T>(TimedValue<T> typedVariant)
VisitSpecific(CustomTimedValue typedVariant)
评论