通用重载解析

8

我有以下场景:

class Foo { }

class Foo<T> : Foo { }

然后有两种方法。
void DoStuff(Foo foo) 
{
     DoStuffImpl(foo);
}

void DoStuffImpl(Foo foo) 
{ 
     Console.WriteLine("A");
}    
void DoStuffImpl<T>(Foo<T> foo) 
{ 
     Console.WriteLine("B");
} 

void Main() 
{
     DoStuff(new Foo<int>()); // prints A
}

(注意,以下代码是在浏览器中编写的,但描述的是我面临的情况)

如何调用泛型方法并输出B?

如果没有反射,是否可以完成这项任务?我有一些通过反射实现的想法,但如果存在更简洁的解决方案,我将非常感激。

注意:我不能将DoStuff设置为泛型,因为它会与WCF产生冲突,开放泛型类型不被允许。


3
方法解析发生在编译时而不是运行时。一旦进入DoStuff的主体,编译器就无法知道它最初是一个Foo<T>。 - Kirk Woll
在DoStuffImpl的第二个重载中,T被声明在哪里? - Eric Lippert
@Eric,应该是DoStuffImpl<T>(Foo<T> foo),我的错。 - andreialecu
顺便说一句,如果在Foo<T>上操作产生的结果与在Foo上操作产生的结果显著不同,那么您可能违反了透明度。 DoStuff(Foo)应该能够操作任何类型的Foo,这意味着从Foo派生的所有类都应该按照DoStuff(foo)所依赖的Foo指定的行为执行。 实质上,您正在尝试在FooFoo<T>的实现之外为DoStuff方法提供多态行为。 您可能需要重新考虑导致此问题的责任分离。 - Dan Bryant
@Dan:这只是WCF无法使用泛型参数的一种解决方法。我实际上想要有两个不同版本的DoStuff方法,一个是通用的,一个不是,但是WCF不允许您在操作契约中拥有开放式泛型类型参数。因此,通过这种方法,我可以欺骗它接受Foo<T>。 - andreialecu
2个回答

18

我假设您已经了解为什么会发生这种情况。如果没有,请阅读我的重载解析文章,并让我知道是否还不清楚。

如果您正在使用C#4,则可以使用动态类型:

void DoStuff(Foo foo) 
{
    dynamic d = foo;
    DoStuffImpl(d);
}

请注意,这不仅具有动态参数--通过将foo限制为类型为Foo或子类,我们将始终有一个有效的DoStuffImpl可供调用...只是最好的方法将在执行时确定,而不是编译时。

如果你被困在C# 4之前,你可能可以使用双重分派来实现:

class Foo
{
    public virtual void CallStuffImpl(FooImplType x)
    {
        x.DoStuffImpl(this);
    }
}

class Foo<T> : Foo
{
    public override void CallStuffImpl(FooImplType x)
    {
        // Looks like it's redundant, but isn't! "this" is
        // known to be Foo<T> rather than Foo
        x.DoStuffImpl(this);
    }
}

然后:

void DoStuff(Foo foo) 
{
    foo.CallStuffImpl(this); // Let it dispatch appropriately
}

+1,很酷,我从没想过动态可以用于重载解析。 - Kirk Woll
谢谢Jon。是的,我明白为什么会发生这种情况。虽然我还没有使用C# 4.0,但如果它可以解决这个问题,那么这将是一个很好的理由去转换到它! - andreialecu

17

重载决策在编译时进行。编译"DoStuff"时,它已经决定了要调用哪个版本的DoStuffImpl,并且这是基于编译时可用的信息而不是运行时可用的信息做出的决策。

C#中有四种方法分派方式:

  • 静态分派在编译时选择一个静态方法。在运行时,调用所选方法。

  • 实例分派在编译时选择一个实例方法。在运行时,调用所选方法。(这是非虚拟实例方法和使用"base"调用的虚拟方法所使用的分派形式)

  • 虚拟分派在编译时选择一个实例方法。在运行时,调用对象的运行时类型上最具体的覆盖该方法的版本

  • 动态分派在编译时不进行任何操作(*).在运行时,编译器会重新启动并在运行时执行编译时分析,生成全新的代码,这就好像你在第一次编译时就正确地完成了它。这样做的成本非常昂贵;幸运的是,结果被缓存,因此在第二次调用时,您不会再次获取所有分析和代码生成的成本。

动态分派仅在C# 4或任何版本的VB中可用。

(*) 这并不完全正确;即使方法的参数是动态的,在某些情况下,编译器也可以在编译时进行分析。细节很复杂。


谢谢Eric,看来是时候转向C# 4.0/VS2010了,动态调度似乎是最干净的解决方案。 - andreialecu
“编译器重新启动”。这只是对DLR所做的事情的一种比喻,对吗? - Hans Passant
@Hans:问题不在于DLR本身,而是C#绑定器实现了DLR接口。(特别是我不认为它在dlr.codeplex.com上可用。) - Jon Skeet
1
细节很复杂。我闻到了一篇博客文章,或者可能是整个系列的味道? - Ben Voigt
@Hans:好的,一个编译器再次启动。C#运行时绑定器是从命令行编译器库中相关代码移植而来的。 - Eric Lippert
@Ben:查看克里斯·伯罗斯的博客系列。 - Eric Lippert

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