服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C# - C# 构造函数如何调用虚方法

C# 构造函数如何调用虚方法

2022-09-21 14:08NiKaFace C#

这篇文章主要介绍了C# 构造函数如何调用虚方法,文中讲解非常详细,示例代码帮助大家更好的理解和学习,感兴趣的朋友可以了解下

谜题

在C#中,用virtual关键字修饰的方法(属性、事件)称为虚方法(属性、事件),表示该方法可以由派生类重写(override)。虚方法是.NET中的重要概念,可以说在某种程度上,虚方法使得多态成为可能。

然而虚方法的使用却存在着很大学问,如果滥用的话势必对程序产生很大的负面影响。比如下面这个例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Puzzle
{
  public Puzzle()
  {
    Name = "Virtual member call in constructor";
    Solve();
  }
 
  public virtual string Name { get; set; }
 
  public virtual void Solve()
  {
  }
}

如果您的Visual Studio没有安装ReSharper,那么上面的代码不会有任何异常。但如果安装了,在构造函数内部给Name赋值和调用Solve时就会在下面产生一个波浪线,即警告:virtual member call in constructor。

C# 构造函数如何调用虚方法

这是什么原因呢?我们在构造函数中调用虚方法,碍着ReSharper什么事儿了?

其实这个警告就是提醒我们不要在非封闭类型的构造函数内调用虚方法或虚属性。但为什么这样做不合适呢?在解惑之前,我们先来了解两个概念。

类型的初始化顺序

我们先来看这样一段代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base
{
  public Base()
  {
    Console.WriteLine("Base constructor");
  }
}
class Derived : Base
{
  public Derived()
  {
    Console.WriteLine("Derived constructor");
  }
}
static class Program
{
  static void Main()
  {
    new Derived();
    Console.Read();
  }
}

猜一猜它的输出结果是什么?

你也许已经猜到了,它的结果是:

Base constructor
Derived constructor

我们在初始化一个对象时,总是会先执行基类的构造函数,然后再执行子类的构造函数。

虚方法调用

我们再来看一段代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Base
{
  public void M()
  {
    Console.WriteLine("Base.M");
  }
 
  public virtual void V()
  {
    Console.WriteLine("Base.V");
  }
}
class Derived : Base
{
  public new void M()
  {
    Console.WriteLine("Derived.M");
  }
 
  public override void V()
  {
    Console.WriteLine("Derived.V");
  }
}
static class Program
{
  static void Main()
  {
    var d = new Derived();
    Base b = d;
    b.M();
    b.V();
    d.M();
    d.V();
    Console.Read();
  }
}

再来猜一猜输出结果吧。

貌似应该是:

Base.M
Base.V
Derived.M
Derived.V

但运行一下会发现,真正的结果是这样的:

Base.M
Derived.V
Derived.M
Derived.V

这是为什么呢?

原来对于非虚方法调用,编译器会进行一些额外的“动作”。比如找出所调用对象的实际类型,以访问正确的方法表(调用b.V()的时候就会找到变量b的实际类型Derived,从而输出Derived.V)。

解惑

现在回到我们最初的谜题,virtual member call in constructor。结合以上两个知识点,会有哪些发现?

我们稍微改造一下虚方法调用的那个例子。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Foo
{
  public Foo(string s)
  {
    Console.WriteLine(s);
  }
  public void Bar() { }
}
 
class Base
{
  public Base()
  {
    V(); // Virtual member call in constructor
  }
  public virtual void V()
  {
    Console.WriteLine("Base.V");
  }
}
class Derived : Base
{
  private Foo foo;
  public Derived()
  {
    foo = new Foo("foo in Derived");
  }
 
  public override void V()
  {
    Console.WriteLine("Derived.V");
    foo.Bar(); // will throw NullReferenceException
  }
}

在Base的构造函数中调用虚方法V()时,ReSharper会给出virtual member call in constructor的警告。这是因为V可以在Base的任意子类中被改写(override),而这种改写,很有可能使得它依赖于自己的构造函数,如上例所示。而由于之前提到的类型初始化顺序,在执行Base b = new Derived();这样的代码时,Base的构造函数要早于Derived的构造函数执行,因此在执行到foo.Bar()时foo还是个空引用。

明白了吗?我们来简单总结一下。Virtual member call in constructor的警告是因为,对于Base b = new Derived();这样的代码:

  1. 基类构造函数的执行要早于子类构造函数
  2. 基类构造函数中对于虚方法的调用,实际调用的是子类中重写的虚方法

因此,ReSharper会警告我们,这么做存在隐患。

我们能完全避免这么做吗?很遗憾,答案是不能。比如如果项目中使用了NHibernate,框架本身要求ORM实体类中,所有与数据库列具有对应关系的属性都必须为虚属性。这是因为NHibernate为了实现延迟加载,会为每个实体类生成proxy,这些proxy需要重写实体类中属性的getter/setter。而有些时候,为了业务需要,我们不得不在实体类的构造函数中对这些属性进行某些操作(比如初始化)。

我认为这么做是技术选型所致的必然结果,是完全可以接受的。但我们要注意,在代码中保证那些可能会被继承的实体,在子类中重写那些虚属性时,不要依赖于子类自身的构造函数(这几乎是可以保证的,因为与数据库列映射的属性,只能是最简单的getter/setter)。

以上就是C# 构造函数如何调用虚方法的详细内容,更多关于C# 构造函数内调用虚方法的资料请关注服务器之家其它相关文章!

原文链接:https://www.cnblogs.com/YourDirection/p/12990372.html

延伸 · 阅读

精彩推荐
  • C#c#操作sql server2008 的界面实例代码

    c#操作sql server2008 的界面实例代码

    这篇文章主要介绍了c#操作sql server2008 的界面实例代码,非常不错,具有参考借鉴价值,需要的朋友可以参考下...

    sun_boy_05842021-12-29
  • C#使用Linq注意事项避免报错的方法

    使用Linq注意事项避免报错的方法

    这篇文章主要介绍了使用Linq注意事项避免报错的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们...

    森大科技8932022-08-16
  • C#C#编程和Visual Studio使用技巧(下)

    C#编程和Visual Studio使用技巧(下)

    本文将介绍10个C#编程和Visual Studio IDE使用技巧,这也是近期我们发布的系列文章的下篇。...

    黄永兵6632021-10-29
  • C#C#实现启用与禁用本地网络的方式小结【3种方式】

    C#实现启用与禁用本地网络的方式小结【3种方式】

    这篇文章主要介绍了C#实现启用与禁用本地网络的方式,结合实例形式总结分析了使用Hnetcfg.dll、Shell32.dll及setupapi.dll三种启用与禁用本地网络的操作方法,需...

    饅頭8752021-11-30
  • C#C# [ImportDll()] 知识小结

    C# [ImportDll()] 知识小结

    今天小编就为大家分享一篇关于C# [ImportDll()] 知识小结,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看...

    chenqiangdage4792022-03-08
  • C#c# 两个数组比较,将重复部分去掉,返回不重复部分的实现

    c# 两个数组比较,将重复部分去掉,返回不重复部分的实现

    下面小编就为大家带来一篇c# 两个数组比较,将重复部分去掉,返回不重复部分的实现方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一...

    C#教程网10352021-12-14
  • C#C#读写EXCEL单元格的问题实现

    C#读写EXCEL单元格的问题实现

    这篇文章主要介绍了C#读写EXCEL单元格的问题实现,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...

    微时空4682022-07-20
  • C#C#无边框窗体实现以及拖动代码

    C#无边框窗体实现以及拖动代码

    我们给大家分享了关于C#无边框窗体实现以及拖动代码,大家在程序设计的时候如果用的到一起跟着小编学习下吧。...

    C#教程网5712022-02-21