【问题标题】:C# & generics - why is method in base class called instead of new method in derived class?C# 和泛型 - 为什么调用基类中的方法而不是派生类中的新方法?
【发布时间】:2012-05-31 05:50:11
【问题描述】:

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

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

更新:但是,当约束使用像where T : IBase 这样的接口时,会调用基类中的方法(而不是接口中的方法,这也是不可能的)。
这意味着系统实际上能够检测到远远超出类型约束的类型!那么为什么在类类型约束的情况下它不超出类型约束呢?
这是否意味着实现该接口的基类中的方法具有该方法的隐式覆盖关键字?

测试代码:

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<T>
    where T : Base
{
    public void CallMethod(T obj)
    {
        obj.Method();  //calls Base.Method()
    }

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

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

    public void CallMethod2<T2>(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>(T obj)
        where T : Base
    {
        obj.Method();  //calls Base.Method()
    }

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

public class NewMethod
{
    unsafe static void Main(string[] args)
    {
        Generic<Derived> genericObj = new Generic<Derived>();
        GenericWithInterfaceConstraint<Derived> genericObj2 = new GenericWithInterfaceConstraint<Derived>();
        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

【问题讨论】:

  • 是什么让你认为接口受限的没有调用IBase.Method?尝试将Obj 转换为IBase 并在其上调用Method
  • 请注意,如果您改写public class Derived : Base, IBase,则会有所不同,即在派生类的声明中也重复该接口。这称为接口重新实现See the spec.

标签: c# generics inheritance method-call overriding


【解决方案1】:

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

要理解为什么会这样,想象一下如果不是这样。如果类Base 声明了一个方法int Foo(),而一个类Derived:Base 声明了一个new string Foo(),会发生什么。如果具有约束T:Base 的泛型类尝试在T 类型的对象上调用方法Foo,该方法的返回类型应该是什么?

【讨论】:

  • 返回类型很好。 (您可以继续讨论方法绑定以及虚拟调用和插槽,但我们中的一些人需要一个明显的突破性示例来弄清楚。)
  • @Rawling:讨论绑定的原因是要明确从编译器的角度来看,泛型类型T 约束为Foo 在许多方面表现得更像Foo 而不是类似在运行时可以替换 T 的实际类型。 .net 中的泛型看起来像 C++ 模板,但它们有着根本的不同。
  • @supercat:谢谢,我想如果没有新方法的返回类型问题,那么设计可能会有所不同。在我看来,这是这个设计决定的单一 原因。关于绑定和不重载等的所有其他内容都只是说明结果。我也应该在我知道存在重载的问题中更清楚地说明。
  • @RolandPihlakas:我怀疑这是唯一的原因,实际上。即使要制定允许编译器解决此类问题带来的所有潜在歧义的规则,让泛型表现得好像它们是运行时绑定的,也需要针对其中使用的每种泛型类型组合重新编译每个泛型方法.这将严重损害性能,同时提供的收益相对较小。
  • @supercat:是的,不可避免地会为不同的泛型类型编译多个方法。但是 C++ 也这样做,不是吗?我希望使用泛型能够让我摆脱虚拟调用。似乎情况并非如此,泛型的唯一与性能相关的用途是避免装箱值类型。对于类,使用泛型参数类型而不是接口(或具有虚拟方法的基类)不能提供性能优势。
【解决方案2】:

这是因为T 被限制为具有Base 的语义。我无法准确告诉您类型绑定在运行时发生了什么,但这是我有根据的猜测。

您没有正确覆盖该方法,而是通过“new”隐藏,如果您使用对基类的引用,您将绕过任何隐藏。这就是隐藏失败的地方。

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

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

要正确覆盖方法并让此代码正常工作,请在基类中将该方法标记为virtual,在派生类中将其标记为override

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

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

您可以证明这一点,将您的通用约束更改为where T : Derived,它应该会命中“新”成员。

【讨论】:

    【解决方案3】:

    这是由于运算符 new 的性质造成的: 新的不像覆盖,创建一个与基本方法同名的函数,它掩盖基本方法但不覆盖它。

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

    【讨论】:

      【解决方案4】:

      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
      

      编辑:已更新以匹配问题的更新输出。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-11-11
        • 2013-07-17
        • 1970-01-01
        • 2023-03-28
        • 1970-01-01
        • 1970-01-01
        • 2019-03-08
        • 2011-04-27
        相关资源
        最近更新 更多