C#&泛型 - 为什么在基类中调用方法而不是派生类中的新方法?

如果通用类型参数(调用类或调用方法)受到约束,其中T:Base ,则不调用T == Derived中的新方法,而是调用Base中的方法。

为什么类型T在方法调用中被忽略,即使它在运行时间之前应该知道?

Update: BUT, when the constraint is using an interface like where T : IBase the method in Base class is called (not the method in interface, which is also impossible).
So that means the system actually is able to detect the types that far and go beyond the type constraint! Then why doesn't it go beyond the type constraint in case of class-typed constraint?
Does that mean that the method in Base class that implements the interface has implicit override keyword for the method?

测试代码:

public interface IBase
{
    void Method();
}

public class Base : IBase 
{
    public void Method()
    {

    }
}

public class Derived : Base
{
    public int i = 0;

    public new void Method()
    {
        i++;
    }
}

public class Generic
    where T : Base
{
    public void CallMethod(T obj)
    {
        obj.Method();  //calls Base.Method()
    }

    public void CallMethod2(T2 obj)
        where T2 : T
    {
        obj.Method();  //calls Base.Method()
    }
}

public class GenericWithInterfaceConstraint
    where T : IBase
{
    public void CallMethod(T obj)
    {
        obj.Method();  //calls Base.Method()
    }

    public void CallMethod2(T2 obj)
        where T2 : T
    {
        obj.Method();  //calls Base.Method()
    }
}

public class NonGeneric
{
    public void CallMethod(Derived obj)
    {
        obj.Method();  //calls Derived.Method()
    }

    public void CallMethod2(T obj)
        where T : Base
    {
        obj.Method();  //calls Base.Method()
    }

    public void CallMethod3(T obj)
        where T : IBase
    {
        obj.Method();  //calls Base.Method()
    }
}

public class NewMethod
{
    unsafe static void Main(string[] args)
    {
        Generic genericObj = new Generic();
        GenericWithInterfaceConstraint genericObj2 = new GenericWithInterfaceConstraint();
        NonGeneric nonGenericObj = new NonGeneric();
        Derived obj = new Derived();

        genericObj.CallMethod(obj);  //calls Base.Method()
        Console.WriteLine(obj.i);

        genericObj.CallMethod2(obj);  //calls Base.Method()
        Console.WriteLine(obj.i);

        genericObj2.CallMethod(obj);  //calls Base.Method()
        Console.WriteLine(obj.i);

        genericObj2.CallMethod2(obj);  //calls Base.Method()
        Console.WriteLine(obj.i);

        nonGenericObj.CallMethod(obj);  //calls Derived.Method()
        Console.WriteLine(obj.i);

        nonGenericObj.CallMethod2(obj);  //calls Base.Method()
        Console.WriteLine(obj.i);

        nonGenericObj.CallMethod3(obj);  //calls Base.Method()
        Console.WriteLine(obj.i);

        obj.Method();  //calls Derived.Method()
        Console.WriteLine(obj.i);
    }
}

输出:

0
0
0
0
1
1
1
2
0
额外 编辑
意见: 1
是什么让你认为界面约束的不是调用 IBase.Method ?尝试将 Obj 转换为 IBase 并在其上调用 Method
额外 作者 supercat,
请注意,如果使用 public class Derived:Base,IBase 来编写它,那么它会有所不同,即在派生类的声明中重复该接口。这被称为接口重新实现查看规范
额外 作者 Jeppe Stig Nielsen,

4 答案

除了使用 dynamic 对象外,C#在编译时总是绑定方法 - 即使在使用泛型时也是如此。虚拟方法调用绑定到虚拟方法插槽而不是实现方法,以便在派生类对象上执行时,它们将被引导到派生类实现;虽然插槽指向的方法将在运行时确定,但对插槽的绑定发生在编译时。如果派生类方法被声明为 new 而不是 override ,那么使用派生类绑定的代码将使用derived-class方法,但使用基类将使用基类方法。

为了理解为什么会出现这种情况,想象一下如果不是这样。如果Class Base 声明了一个方法 int Foo(),并且类 Derived:Base 声明了一个新的字符串Foo( )</代码>。如果一个约束 T:Base 的泛型类试图在 T 类型的对象上调用方法 Foo ,该方法的返回类型应该是什么是?

0
额外
返回类型的好处。 (你可以继续关于方法绑定和虚拟调用和插槽,但我们中的一些人需要一个明显的突破性例子来解决它。)
额外 作者 Rawling,
@Rawling:讨论绑定的原因是要明确从编译器的角度来看,限制为 Foo 的泛型类型 T 在很多方面会表现得更像一个 Foo ,而不像在运行时可能被替换为 T 的实际类型。 .net中的泛型看起来像C ++模板,但它们根本不同。
额外 作者 supercat,
@RolandPihlakas:我怀疑这实际上是单一原因。即使有人制定了可以让编译器解决由此类问题引起的所有潜在歧义的规则,但是仿制药的行为就像是运行时限制一样,将要求每种通用方法都针对其中使用的每种通用类型组合进行重新编译。这会严重损害业绩,同时提供相对较少的利益。
额外 作者 supercat,
@RolandPihlakas:使用泛型可以在编译时执行许多类型的检查,而不是运行时。例如,如果代码从 IEnumerable 中读取一个项目,它可以将该项目用作 Animal ,而无需在运行时检查它是否为。这既省去了代码在运行时检查类型的情况(这需要花费一些时间),也消除了运行时类型检查可能失败的可能性。
额外 作者 supercat,
@RolandPihlakas:同样重要的一点是,要编译一个C ++程序,编译器必须能够识别程序执行期间可能创建的所有模板类型,编译后的程序的大小将成比例增长到方法可以使用的不同类型组合的数量。在C#中,即使不使用反射,也可以编写一个程序,该程序将采用任意长度的数字字符串(例如, 24601 ),并调用带有嵌套泛型类型的泛型例程(例如 X2 >>>> </代码>)。由于嵌套泛型类型的数量...
额外 作者 supercat,
......这样一个例程可以传递给泛型例程,它将大大超过宇宙中的原子数量,编译器将无法为它们预先生成代码。 JITter有可能为每种生成的类型编译一次通用方法,但性能成本可能远远超过这个收益。
额外 作者 supercat,
@RolandPihlakas:微软可能以这种方式设计了JIT编译器,但它的运行时间性能成本会很高,并且实际上并没有很多情况下它可以完成任何有用的功能,而使用组合虚拟函数和Reflection的编译时绑定。尽管.net不会为它使用的每个类类型组合重新编译泛型代码,但泛型类为每个泛型类型参数组合保存一组单独的静态变量。
额外 作者 supercat,
@supercat:谢谢,我想如果没有新方法的返回类型的问题,那么设计可能会有所不同。这在我看来是这个设计决定的单一原因。关于绑定和不超载的其他一切都只是说明了后果。我也应该在问题中更清楚地说明我知道超载的存在。
额外 作者 Roland Pihlakas,
@supercat:是的,那么为不同的泛型类型编译多个方法是不可避免的。但是C ++也这样做,不是吗?我希望使用泛型使我能够摆脱虚拟呼叫。似乎并非如此,唯一与性能相关的泛型使用是避免装箱值类型。对于类,使用泛型参数类型而不是接口(或使用虚拟方法的基类)不能提供性能优势。
额外 作者 Roland Pihlakas,
@supercat:是的,我希望JIT在首次初始化时生成泛型方法/类型。类似于一次性静态类型初始化。
额外 作者 Roland Pihlakas,

这是因为 T 被限制为具有 Base 的语义。我无法确切地告诉你在运行时绑定类型是怎么回事,但这是我的猜测。

你没有正确覆盖该方法,而是通过“新”隐藏,如果你使用引用到基类,你绕过任何隐藏。这是隐藏的地方。

隐藏其他成员的成员只有在使用对隐藏类型的引用时才会受到尊重。您可以通过使用对基类的引用来绕过隐藏的成员:

var derived = new Derived();
var baseRef = (Base)derived;
baseRef.Method();//calls Base.Method instead of Derived.Method.

要正确覆盖方法并使此代码有效,请在基类中将该方法标记为 virtual ,并在派生类中将其覆盖。

class Base
{
    public virtual void Method() {}
}

class Derived : Base
{
    public override void Method() {}
}

您可以证明这一点,将您的通用约束更改为其中T:Derived ,并且它应该碰到“new”成员。

0
额外

这是由于新运营商的性质: 新的不同于重写,创建一个与基本名称相同的函数,该函数掩盖基本方法但不覆盖它。

因此,如果没有适当的转换,如果引用是Base类型,则将调用原始方法。

0
额外

new 关键字只是简单地隐藏了方法,而不是重载它。您的非通用 CallMethod 看起来像预期的那样工作的原因是因为方法签名需要一个 Derived 而不是 Base 。

泛型并不是真正的罪魁祸首。如果将方法签名更改为 CallMethod(Base obj),则会看到与通用实现相同的“意外”行为并获得以下输出:

0
0
0
0
0
0
0
1

如果您使 Base.Method 变为虚拟,并使用 Derived.Method 覆盖它,如下所示:

public class Base 
{
    public virtual void Method()
    {

    }
}

public class Derived : Base
{
    public int i = 0;

    public override void Method()
    {
        i++;
    }
}

你会得到以下输出:

1
2
3
4
5
6
7
8

Edit: updated to match question's updated output.

0
额外