• 《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。所以我们在使用时默认赋值时一定要对其赋值的时机做到心中有数。

  • 相关阅读:
    文字
    <script type="text/x-template"> 模板
    防xss攻击
    url
    symmfony
    composer
    header 和http状态码
    bootstrap
    linux的设置ip连接crt,修改主机名,映射,建文件
    Centos上传下载小工具lrzsz
  • 原文地址:https://www.cnblogs.com/wang_yb/p/2083775.html
Copyright © 2020-2023  润新知