《CLR Via C# 第3版》笔记之(四) - 类中字段的默认赋值

简介:

在C#中,除了可以在类的构造函数中初始化私有字段的值,还可以在私有字段定义的地方进行初始化(即默认赋值)。下面讨论默认赋值和在构造函数中赋值的区别,以便更好的在代码中使用这两种赋值。

主要内容:

  • 对代码生成的影响
  • 对代码执行的影响

1. 对代码生成的影响

首先构造两个Class,其中ClassA使用默认赋值的方式,ClassB使用构造函数赋值的方式。

代码如下:

    public class ClassA
    {
        private Int32 a = 123;
        private String b = "abc";
        private Object c = new object();

        public ClassA()
        {
        }
        public ClassA(int aa)
        {
            a = aa;
        }
    }

    public class ClassB
    {
        private Int32 a;
        private String b;
        private Object c;

        public ClassB()
        {
            a = 123;
            b = "abc";
            c = new object();
        }
        public ClassB(int aa)
        {
            a = aa;
        }
    } 

编译成dll后,再用ILSpy查看其IL代码,发现ClassA生成的代码比较多。即每个构造函数开始执行处,都会将字段的默认赋值生成IL代码插入其中。

ClassA IL代码如下

.class public auto ansi beforefieldinit ClassA
	extends object
{
	// Fields
	.field private int32 a
	.field private string b
	.field private object c

	// Methods
	.method public hidebysig specialname rtspecialname 
		instance void .ctor () cil managed 
	{
		// Method begins at RVA 0x2073
		// Code size 40 (0x28)
		.maxstack 8

		IL_0000: ldarg.0
		IL_0001: ldc.i4.s 123
		IL_0003: stfld int32 class cnblog_bowen.ClassA::a
		IL_0008: ldarg.0
		IL_0009: ldstr "abc"
		IL_000e: stfld string class cnblog_bowen.ClassA::b
		IL_0013: ldarg.0
		IL_0014: newobj instance void object::.ctor()
		IL_0019: stfld object class cnblog_bowen.ClassA::c
		IL_001e: ldarg.0
		IL_001f: call instance void object::.ctor()
		IL_0024: nop
		IL_0025: nop
		IL_0026: nop
		IL_0027: ret
	} // End of method ClassA..ctor

	.method public hidebysig specialname rtspecialname 
		instance void .ctor (
			int32 aa
		) cil managed 
	{
		// Method begins at RVA 0x209c
		// Code size 47 (0x2f)
		.maxstack 8

		IL_0000: ldarg.0
		IL_0001: ldc.i4.s 123
		IL_0003: stfld int32 class cnblog_bowen.ClassA::a
		IL_0008: ldarg.0
		IL_0009: ldstr "abc"
		IL_000e: stfld string class cnblog_bowen.ClassA::b
		IL_0013: ldarg.0
		IL_0014: newobj instance void object::.ctor()
		IL_0019: stfld object class cnblog_bowen.ClassA::c
		IL_001e: ldarg.0
		IL_001f: call instance void object::.ctor()
		IL_0024: nop
		IL_0025: nop
		IL_0026: ldarg.0
		IL_0027: ldarg.1
		IL_0028: stfld int32 class cnblog_bowen.ClassA::a
		IL_002d: nop
		IL_002e: ret
	} // End of method ClassA..ctor

} // End of class cnblog_bowen.ClassA

  

ClassB IL代码如下

.class public auto ansi beforefieldinit ClassB
	extends object
{
	// Fields
	.field private int32 a
	.field private string b
	.field private object c

	// Methods
	.method public hidebysig specialname rtspecialname 
		instance void .ctor () cil managed 
	{
		// Method begins at RVA 0x20cc
		// Code size 40 (0x28)
		.maxstack 8

		IL_0000: ldarg.0
		IL_0001: call instance void object::.ctor()
		IL_0006: nop
		IL_0007: nop
		IL_0008: ldarg.0
		IL_0009: ldc.i4.s 123
		IL_000b: stfld int32 class cnblog_bowen.ClassB::a
		IL_0010: ldarg.0
		IL_0011: ldstr "abc"
		IL_0016: stfld string class cnblog_bowen.ClassB::b
		IL_001b: ldarg.0
		IL_001c: newobj instance void object::.ctor()
		IL_0021: stfld object class cnblog_bowen.ClassB::c
		IL_0026: nop
		IL_0027: ret
	} // End of method ClassB..ctor

	.method public hidebysig specialname rtspecialname 
		instance void .ctor (
			int32 aa
		) cil managed 
	{
		// Method begins at RVA 0x20f5
		// Code size 17 (0x11)
		.maxstack 8

		IL_0000: ldarg.0
		IL_0001: call instance void object::.ctor()
		IL_0006: nop
		IL_0007: nop
		IL_0008: ldarg.0
		IL_0009: ldarg.1
		IL_000a: stfld int32 class cnblog_bowen.ClassB::a
		IL_000f: nop
		IL_0010: ret
	} // End of method ClassB..ctor

} // End of class cnblog_bowen.ClassB

由此看出,虽然默认赋值的方法比较直观和方便,但是从生成的代码来看,默认赋值的方法会导致代码膨胀,所以不应在以下场合使用:

1)字段比较多的Class

2)构造函数有多个重载版本

2. 对代码执行的影响

通过上面的IL代码,我们发现默认赋值除了会导致代码膨胀,赋值的时机也和在构造函数中对字段的赋值不一样。

我们知道,类的构造函数在执行之前,都会调用其基类的构造函数,由于所以类都默认继承System.Object,所以上面的ClassA和ClassB虽然没有指定基类,

但都继承于System.Object,所以都会调用System.Object的构造函数。

调用System.Object的构造函数的IL代码即为:call instance void object::.ctor()从上面的IL代码中,我们发现:

1)默认赋值方式是在调用System.Object的构造函数前给字段赋值的

2)构造函数中赋值方式是在调用System.Object的构造函数后给字段赋值的 这里的差别虽然很小,但是有时却会导致代码产生不同的结果,从而带来潜在的bug。

这两种赋值方式在什么情况下会导致执行结果不同呢?

根据其赋值时机的不同,我们可以推断在如下情况下,两种赋值方式的执行结果不同。

基类中调用虚方法并且如果子类覆盖(override)了此虚方法,那么此虚方法中的字段就有可能已经初始化或者未初始化。

第一种情况 (默认赋值的方式)

    class Test
    {
        static void Main()
        {
            // 第一步:调用SubClass的构造函数
            SubClass sub = new SubClass();

            Console.ReadKey(true);
        }
    }

    public class BaseClass
    {
        // 第四步:调用基类构造函数,其中虚方法Print已经被子类覆盖
        public BaseClass()
        {
            Print();
        }

        public virtual void Print()
        {
            Console.WriteLine("Base class initilized!");
        }
    }

    public class SubClass : BaseClass
    {
        // 第三步:对sub_a,sub_b,obj进行赋值,然后再调用基类构造函数
        private Int32 sub_a = 123;
        private String sub_b = "abc";
        private Object obj = new object();

        // 第二步:由于是默认赋值的方式,所以先将sub_a,sub_b,obj赋值后再调用基类构造函数
        public SubClass()
        {
        }

        // 第五步:调用被覆盖的Print方法,由于obj已被赋值,所以进入else分支去执行
        public override void Print()
        {
            if (null == obj)
                Console.WriteLine("Sub class is uninitilize!");
            else
            {
                Console.WriteLine("a= " + sub_a);
                Console.WriteLine("b= " + sub_b);
                Console.WriteLine("Sub class was initilized!");
            }
        }
    }

执行结果如下,执行过程可以参见上面代码中的注释

image

第二种情况(构造函数中对字段赋值的方式)

    class Test
    {
        static void Main()
        {
            // 第一步:调用SubClass的构造函数
            SubClass sub = new SubClass();

            Console.ReadKey(true);
        }
    }

    public class BaseClass
    {
        // 第三步:调用基类构造函数,其中虚方法Print已经被子类覆盖
        public BaseClass()
        {
            Print();
        }

        public virtual void Print()
        {
            Console.WriteLine("Base class initilized!");
        }
    }

    public class SubClass : BaseClass
    {
        private Int32 sub_a;
        private String sub_b;
        private Object obj;

        // 第二步:由于是在构造函数对字段辅助的方式,所以先默认调用基类构造函数
        public SubClass()
        {
            // 第五步:基类构造函数执行完后,进入下面的赋值
            sub_a = 123;
            sub_b = "abc";
            obj = new object();
        }

        // 第四步:调用被覆盖的Print方法,由于obj还未被赋值,所以进入if分支去执行
        public override void Print()
        {
            if (null == obj)
                Console.WriteLine("Sub class is uninitilize!");
            else
            {
                Console.WriteLine("a= " + sub_a);
                Console.WriteLine("b= " + sub_b);
                Console.WriteLine("Sub class was initilized!");
            }
        }
    }

执行结果如下,执行过程可以参见上面代码中的注释

image

小小的赋值,也会导致意外的bug。所以我们在使用时默认赋值时一定要对其赋值的时机做到心中有数。




本文转自wang_yb博客园博客,原文链接:http://www.cnblogs.com/wang_yb/archive/2011/06/17/2083775.html,如需转载请自行联系原作者

目录
相关文章
|
2月前
|
开发框架 .NET C#
C#|.net core 基础 - 删除字符串最后一个字符的七大类N种实现方式
【10月更文挑战第9天】在 C#/.NET Core 中,有多种方法可以删除字符串的最后一个字符,包括使用 `Substring` 方法、`Remove` 方法、`ToCharArray` 与 `Array.Copy`、`StringBuilder`、正则表达式、循环遍历字符数组以及使用 LINQ 的 `SkipLast` 方法。
|
3月前
|
存储 C# 索引
C# 一分钟浅谈:数组与集合类的基本操作
【9月更文挑战第1天】本文详细介绍了C#中数组和集合类的基本操作,包括创建、访问、遍历及常见问题的解决方法。数组适用于固定长度的数据存储,而集合类如`List<T>`则提供了动态扩展的能力。文章通过示例代码展示了如何处理索引越界、数组长度不可变及集合容量不足等问题,并提供了解决方案。掌握这些基础知识可使程序更加高效和清晰。
86 2
|
2月前
|
算法 安全 测试技术
C#——刘铁猛笔记
C#——刘铁猛笔记
53 0
|
2月前
|
Java 程序员 C#
【类的应用】C#应用之派生类构造方法给基类构造方法传参赋值
【类的应用】C#应用之派生类构造方法给基类构造方法传参赋值
14 0
|
3月前
|
C# 数据安全/隐私保护
C# 一分钟浅谈:类与对象的概念理解
【9月更文挑战第2天】本文从零开始详细介绍了C#中的类与对象概念。类作为一种自定义数据类型,定义了对象的属性和方法;对象则是类的实例,拥有独立的状态。通过具体代码示例,如定义 `Person` 类及其实例化过程,帮助读者更好地理解和应用这两个核心概念。此外,还总结了常见的问题及解决方法,为编写高质量的面向对象程序奠定基础。
28 2
|
4月前
|
C#
C#中的类和继承
C#中的类和继承
45 6
|
4月前
|
Java C# 索引
C# 面向对象编程(一)——类
C# 面向对象编程(一)——类
35 0
|
4月前
|
开发框架 .NET 编译器
C# 中的记录(record)类型和类(class)类型对比总结
C# 中的记录(record)类型和类(class)类型对比总结
|
6月前
|
开发框架 .NET 编译器
程序与技术分享:C#基础知识梳理系列三:C#类成员:常量、字段、属性
程序与技术分享:C#基础知识梳理系列三:C#类成员:常量、字段、属性
41 2
|
6月前
|
存储 安全 C#
C# 类的深入指南
C# 类的深入指南