.Net 继承和方法重载
Posted
技术标签:
【中文标题】.Net 继承和方法重载【英文标题】:.Net inheritance and method overloading 【发布时间】:2014-11-24 20:40:43 【问题描述】:这是一个代码示例:
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)
有人能解释一下为什么调用派生类的方法(而不是基类方法)吗?我需要对这种情况进行详细解释。我将不胜感激任何有用文章的链接。
谢谢。
【问题讨论】:
我认为你的问题也应该解释你预期会发生什么,以及为什么。 @EhsanSajjad:错了。 OP 不会覆盖问题源代码中的任何内容。 是的,因为它不是虚拟方法吗? 仅供参考,我认为Jon Skeet's blog post 涵盖了这个问题以及其他有关方法重载的问题。 【参考方案1】:具体的措辞和位置因规范的不同版本而异,但例如here可以阅读:
构造方法调用的候选方法集。从与 M 相关联的一组方法开始,这些方法是通过先前的成员查找(第 7.3 节)找到的,该集合被缩减为适用于参数列表 A 的那些方法。集合缩减包括应用以下规则到集合中的每个方法 TN,其中 T 是声明方法 N 的类型:
如果 N 不适用于 A(第 7.4.2.1 节),则从集合中删除 N。
如果 N 适用于 A(第 7.4.2.1 节),则在 T 的基类型中声明的所有方法都将从集合中删除。
所以,假设我们有DerivedClass
类型的obj
,那么成员方法集包含来自DerivedClass
的void SomeMethod(long)
和来自BaseClass
的void SomeMethod(int)
。
这两种方法都适用,确实void SomeMethod(int)
是更好的重载匹配,但是因为上面引用的最后一句中的规则,一旦发现void SomeMethod(long)
适用,所有来自基类的方法都是从候选集中删除,这意味着不再考虑 void SomeMethod(int)
。
好的,这是规范方面的技术原因。首先在规范中的设计原因是什么?
好吧,想象一下 BaseClass
开始定义为:
public class BaseClass
如果其余代码相同,那么很明显对 obj.SomeMethod(5)
的调用应该调用唯一存在的所谓方法。
现在考虑如果在该代码被编写之后,void SomeMethod(int)
方法被添加到BaseClass
。并且确实考虑到这可能与DerivedClass
在不同的程序集中,并且由单独的作者编写。
现在调用SomeMethod()
的含义已经改变。更糟糕的是,它是否更改取决于给定机器已应用或未应用哪些更新。 (更糟糕的是,由于 C# 重载解析中不使用返回类型,它的更改方式可能会在已编译的代码中产生编译错误:完全破坏性更改。
如果存在来自更多派生类的重载候选者,则排除基类中定义的方法的规则允许更大的保证,即在面对未来的变化时,人们正在调用想要调用的方法。 (当然,如果您打算调用基类方法,您可能会感到惊讶,但是在编码时您可能会发现该问题并使用强制转换来确保您想要的行为是结果)。
这可能会让一些人感到惊讶的结果是:
class Program
static void Main(string[] args)
var obj = new DerivedClass();
obj.SomeMethod(5);
class BaseClass
public virtual void SomeMethod(int a) Console.WriteLine("Base");
class DerivedClass : BaseClass
public override void SomeMethod(int a) Console.WriteLine("Defined in Base, overriden in Derived");
public void SomeMethod(long a) Console.WriteLine("Derived");
这将输出Derived
,因为此规则根据声明方法的位置应用,即使存在来自覆盖的实现。
(该规则正常工作的另一个原因是,当它转换为 CIL 时,调用将包含有关其声明的类的信息。这里的规则是最简单的做事方式。也就是说;1)在 CIL 的设计中应用了类似的逻辑,并且 2)上述使 CIL 的特性成为 C# 人员可以使用的特性,而不是与之对抗的特性)。
【讨论】:
一个很好的理由不在同一组类中使用重载方法和继承。 @IanRingrose 我不会这么说,而是建议不要跨越继承边界。【参考方案2】:var obj = new DerivedClass();
var
关键字只是 C# 中的语法糖;这与以下内容基本相同:
DerivedClass obj = new DerivedClass();
因此您调用DerrivedClass.SomeMethod
,这正是您正在经历的行为。如果您像这样定义变量,您会看到不同:
BaseClass obj = new DerivedClass();
【讨论】:
这本身并不能解释为什么没有调用更好的重载匹配。【参考方案3】:在 cmets 之后编辑: 确实,我可能没有正确回答确切的问题,所以现在让我试试吧:
原始代码中的方法调用匹配两个方法的签名(在基类和派生类中),因为在这种情况下,参数5
可以是int
或long
。然而,基本方法没有标记为virtual
(这将允许覆盖),并且“派生”方法不是真正派生的,因为它没有标记为override
。
但请注意,即使您确实将其标记为 override
,也会出现错误,因为实际上,这两个方法签名并不等效:一个采用 int
,而另一个采用long
类型。这将导致编译时错误消息:“找不到合适的方法来覆盖”。
如果您阅读下面我的原始答案的其余部分,其余部分应该会变得清楚。
原答案:
这里有几点需要注意:
1) 你的方法有不同的签名;一个需要 long,另一个需要 int
2) 你还没有标记你的方法virtual
或override
。
带有一些 cmets 的代码的编辑版本可能会使这些东西的工作原理更加清晰:
internal class Program
private static void Main(string[] args)
var obj = new DerivedClass();
// That is the same as:
//DerivedClass obj = new DerivedClass();
// Will call the base method, since that now matches the
// signature (takes an int parameter). DerivedClass simply
// does not HAVE a method with that signature on it's own:
obj.SomeMethod(5); // will output "base with int"
// Now call the other method, which IS defined in DerivedClass,
// by appending an "l", to mark this as a Long:
obj.SomeMethod(5l); // Will output "derived"
// This would call the base method directly
var obj2 = new BaseClass();
obj2.SomeMethod(5l);
Console.ReadKey();
internal class BaseClass
internal void SomeMethod(int a)
Console.WriteLine("base with int");
// Added method for the example:
// Note that "virtual" allows it to be overridden
internal virtual void SomeMethod(long a)
Console.WriteLine("base with long");
internal class DerivedClass : BaseClass
// Note: Overrides the base method now
internal override void SomeMethod(long a)
Console.WriteLine("derived");
【讨论】:
你错了,obj.SomeMethod(5)
不会输出“base with int”
请注意,通过将方法更改为虚拟,您已经显着改变了 .Net 在这种情况下的行为方式,因此虽然您的回答非常详细,但我不确定您是否在回答原始问题问题。
@decPL 我错过了 Kjartan 更改了方法的定义,假设他使用的是 OP 的原始类。
因此我的评论是这是一个很好的答案,而不是这个问题:)
@decPL 完全正确,即使它非常好,我也无法证明它是合理的 :)【参考方案4】:
来自C# language reference:
7.5.5 函数成员调用
本节描述了在运行时发生的过程 调用特定的函数成员。假设绑定时间 进程已经确定要调用的特定成员, 可能通过对一组候选者应用重载决议 函数成员。
为了描述调用过程,函数成员 分为两类:
静态函数成员。<snip>
实例函数成员。这些是实例方法、实例属性访问器和索引器访问器。实例函数成员 是非虚拟或虚拟的,并且总是在 具体事例。实例由实例计算 表达式,并且它可以在函数成员中作为 这(第 7.6.7 节)。函数成员调用的运行时处理 由以下步骤组成,其中 M 是函数成员,并且, 如果 M 是实例成员,则 E 是实例表达式: 如果 M 是静态函数成员:<snip>
如果 M 是在值类型中声明的实例函数成员:<snip>
如果 M 是在引用类型中声明的实例函数成员: 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();
请注意,编译器会警告您潜在的问题,因为将调用的方法会因您用于定义类实例的类型而异。
【讨论】:
【参考方案5】:我的理解是,由于没有应用覆盖/隐藏,因此在 main() 中实例化了派生类的方法。
在方法覆盖中: 指向子类对象的基类引用变量将调用子类中被覆盖的方法。 派生类方法签名中使用“Override”关键字。
在方法隐藏中: 指向子类对象的基类引用变量将调用基类中的隐藏方法。 “New”关键字用于派生类方法签名。
【讨论】:
以上是关于.Net 继承和方法重载的主要内容,如果未能解决你的问题,请参考以下文章