以下是代码示例:
class Program
{
static void Main(string[] args)
{
var obj = new DerivedClass();
obj.SomeMethod(5);
}
}
class BaseClass
{
internal void SomeMethod(int a) { }
}
class DerivedClass : BaseClass
{
internal void SomeMethod(long a) { }
}
有人可以解释一下为什么派生类调用的方法(而不是基类方法)?我需要详细解释这种情况。我将非常感谢任何有用文章的链接。
感谢。
答案 0 :(得分:21)
准确的措辞和位置因规范的不同版本而异,但例如here可以读取:
构造方法调用的候选方法集。从与先前成员查找(第7.3节)找到的与M关联的方法集开始,该集合被简化为适用于参数列表A的那些方法。集合缩减包括应用以下规则对于集合中的每个方法TN,其中T是声明方法N的类型:
如果N不适用于A(§7.4.2.1),则从集合中删除N.
如果N适用于A(§7.4.2.1),那么在基本类型T中声明的所有方法都将从集合中删除。
因此,假设我们obj
类型为DerivedClass
,则成员方法集包含来自void SomeMethod(long)
的{{1}}和来自DerivedClass
的{{1}} }。
这两种方法都适用,确实void SomeMethod(int)
是一个更好的重载匹配,但由于上面引用的最后一句中的规则,一旦发现BaseClass
适用,所有方法从候选集中删除基类,这意味着不再考虑void SomeMethod(int)
。
好的,这是规范方面的技术原因。首先是在规范中背后的设计原因是什么?
好吧,假设void SomeMethod(long)
开始定义为:
void SomeMethod(int)
如果代码的其余部分是相同的,那么对BaseClass
的调用应该调用唯一存在的如此命名的方法。
现在考虑是否在之后编写了代码,方法public class BaseClass
{
}
已添加到obj.SomeMethod(5)
。并且确实考虑到这可能与void SomeMethod(int)
和另一位作者的组合不同。
现在调用BaseClass
的含义已经改变。更糟糕的是,它的改变与否取决于给定机器已应用或未应用的更新。 (更糟糕的是,由于返回类型未在C#重载决策中使用,因此它的更改方式可能会在已编译的代码中产生编译错误:完全重大更改)。
如果存在来自更派生类的重载候选者,则排除基类中定义的方法的规则允许更好地保证在面对未来的更改时调用方法。 (当然,如果您打算调用基类方法,您可能会感到惊讶,但在编码时您可以捕获该问题并使用强制转换来确保您想要的行为是由此产生的结果。)
这样做的后果可能令人惊讶,但是:
DerivedClass
这会输出SomeMethod()
,因为此规则适用于声明方法的位置,即使存在来自覆盖的实现。
(规则正常工作的另一个原因是,当它被转换为CIL时,调用将包含有关它声明的类的信息。这里的规则是最简单的处理方式。那就是说; 1)在CIL的设计中应用了类似的逻辑,2)上面的内容使得C#的一个特性成为了C#人员的工作,而不是一个人的工作。
答案 1 :(得分:3)
var obj = new DerivedClass();
var
关键字只是C#中的语法糖;这基本上与:
DerivedClass obj = new DerivedClass();
因此您正在调用DerrivedClass.SomeMethod
,这正是您遇到的行为。如果您定义了这样的变量,您会发现不同之处:
BaseClass obj = new DerivedClass();
答案 2 :(得分:2)
评论后编辑: 是的,我可能没有正确回答确切的问题,所以现在让我试试:
原始代码中的方法调用匹配两个方法的签名(在基类和派生类中),因为参数5
可以是int
或{{1 }} 在这种情况下。然而,基本方法没有标记为long
(这将允许覆盖),并且"派生"方法并非真正派生,因为它没有标记为virtual
。
但请注意,即使您 将其标记为override
,也会出现错误,因为实际上,这两个方法签名并不相同:一个需要{{1} 1}},而另一个采用override
类型。这将导致使用messsage的编译时错误:" 找不到合适的方法来覆盖"。
如果您阅读下面的原始答案的其余部分,其余部分应该有希望变得清晰。
原始回答:
这里有几点需要注意:
1)你的方法有不同的签名;一个需要很长时间,另一个需要一个int
2)您尚未标记自己的方法int
或long
。
带有一些注释的代码的编辑版本可能会更清楚地说明这些内容的工作原理:
virtual
答案 3 :(得分:1)
7.5.5函数成员调用
本节介绍在运行时发生的过程 调用特定的函数成员。假设是绑定时间 进程已经确定要调用的特定成员, 可能通过对一组候选者应用重载决策 职能成员。
为了描述调用过程,函数成员 分为两类:
- 静态功能成员。
<snip>
- 实例功能成员。这些是实例方法,实例属性访问器和索引器访问器。实例功能成员 非虚拟或虚拟,并始终在a上调用 特殊情况。实例由实例计算 表达式,它在函数成员中可以访问 这(§7.6.7)。函数成员调用的运行时处理 由以下步骤组成,其中M是函数成员, 如果M是实例成员,则E是实例表达式:
- 如果M是静态函数成员:
<snip>
- 如果M是在值类型中声明的实例函数成员:
<snip>
- 如果M是在reference-type中声明的实例函数成员:
- 评估E.如果此评估导致异常,则不执行进一步的步骤。
- 参数列表按照§7.5.1。
中的描述进行评估- 如果E的类型是值类型,
<snip>
- 检查E的值是否有效。如果E的值为null,则抛出System.NullReferenceException,并且不再执行其他步骤 执行。
- 确定要调用的函数成员实现:
- 如果E的绑定时间类型是接口,
<snip>
- 否则,如果M是虚拟功能成员,
<snip>
- 否则,M是非虚函数成员,要调用的函数成员是M本身。
- 调用在上面步骤中确定的函数成员实现。 E引用的对象成为引用的对象 通过这个。
1.6.6.4虚拟,覆盖和抽象方法还有更多内容
调用虚方法时,实例的运行时类型 调用发生的位置决定了实际的方法 要调用的实现。在非虚拟方法调用中, 实例的编译时类型是决定因素。
所以当你编译你的代码时,你正在使用的变量的类型决定了所谓的方法。
public class A { public void WhoAreYou() { Console.WriteLine("A"); } }
public class B : A { public void WhoAreYou() { Console.WriteLine("B"); } }
internal class Program
{
private static void Main(string[] args)
{
(new B() as A).WhoAreYou(); // "A"
(new B()).WhoAreYou(); // "B"
Console.ReadLine();
}
请注意,编译器会警告您潜在的问题,因为将调用的方法因您用于定义类实例的类型而异。
答案 4 :(得分:1)
我的理解是,由于未应用覆盖/隐藏,因此在main()中实例化时会调用派生类的方法。
在方法覆盖中: 指向子类对象的基类引用变量将调用子类中的重写方法。 “覆盖”关键字用于派生类方法签名。
方法隐藏: 指向子类对象的基类引用变量将调用基类中的隐藏方法。 “New”关键字用于派生类方法签名。