代码改变世界

【读书笔记】《框架设计(第2版)CLR Via C#》中两个比较有趣的知识点

2009-12-20 17:18  GUO Xingwang  阅读(2615)  评论(11编辑  收藏  举报

  本季度公司要求阅读《框架设计(第2版)CLR Via C#》这本书,由于前两个月一直很忙,也没有时间阅读,偶尔阅读也是很晚回家以后临睡前拿起这经典之作读那么一个小节。最近利用周末可以说一鼓作气的看了大半本,感触很深。之前,这本书我阅读过第一版,那时好像叫《.NET框架设计》,当时就特别钦佩JR大叔的技术功底和写作技巧,正向书中有人评论的那样,JR确实适合于把枯涩难懂的概念用简短的语言描述清楚。还有下个季度需要阅读的《WINDOWS核心编程》更是经典中的经典(幸会这本书我阅读过),经典也是毫无疑问的,我这里只是感慨一下,相信阅读过这两本书的人都有这种感慨了。这两本书我觉得是一个WINDOWS程序员应该买来阅读并且摆在案头收藏的。

  在阅读这本书时我发现很多值得思考和有趣的地方,例如:JR关于调用的参数和返回值的建议;位索引器示例;触发事件的事件安全;字符串格式化和字符串的驻留等。尤其是.NET的垃圾回收机制在这本书中讲的很详细。其中有两个知识点是让我感到收获很大的地方而且例子也很详细,我在这里就单独拿出来与大家分享,同时也作为知识点进行总结,这里讲没什么技术含量,大家别BS我。

  第一部分:常量,只读字段,静态字段,静态只读字段区别与比较

  常量:常量就是指永远不会改变的符号,在.Net通过编译以后常量的值会插入到程序集的元数据中,所以常量的类型必须是.Net的基元类型Boolean,Char,Byte,SByte,Int16,Int32,UInt32,Int64,UInt64,Singe,Double,Decimal和String。也就是说在一个程序集A的类中定义一个常量,在另一个程序集B中使用这个常量,当两个程序集编译以后,C#编译器会将这个常量插入到使用这个常量的程序集B的元数据中,而这时使用常量的程序集A对定义常量的程序集B没有运行时的依赖关系了,也就是说在运行时可以删除定义常量的程序集A,同时也说明如果修改这个常量的值再去编译定义常量的程序集A不会改变使用常量的程序集B在运行时获取到的这个常量的值,常量在带来这种好处的同时显然对于程序集的版本控制是很不利的。如果一个程序集需要从另一个程序集中总是获得最新的数据,则不能使用常量,这时可以使用只读字段。此外,常量具有static的含义,只能通过类型访问,不能通过对象访问。

  只读字段:只读字段也是在初始化(这个初始化是在运行时初初始化的)以后在验证过程中不允许修改的字段,只读字段引用的可以是任何类型的对象。但要注意只读字段只有在对象初始化时可以给这个字段赋值,也就是字段在初始化时还具有可写属性,以后这个字段就只读了,字段的只读含义是这个变量的引用不可以改变了,但是具体引用的对象的状态时可以改变的。编译器和验证机制确保只读字段不会被任何其他方法写入,但是我们依然可以使用反射修改只读字段。只读字段属于对象实例的,需要通过对象访问,属于对象状态的一部分。

  静态字段:静态字段是与类型关联的成员(只能通过类型访问,不像Java还可以通过类的实例访问,如果没记错的话),静态字段的初始化是在类型被加载到CLR中时执行的。静态字段可以应用任何类型的对象。

  静态只读字段:静态只读字段与常量比较类似,但是静态只读字段不限于基元类型,可以是.Net中的任何类型,静态只读字段不像常量是在编译时插入到元数据中的,而静态只读字段是在运行时赋值和确定的,也就是对于程序的依赖是一种强依赖关系,具体的值是在运行时去程序集中取到的,而不像常量做一个拷贝插入元数据中。静态只读字段属于类型的,通过类型访问。

  通过反射修改私有字段,下面一个示例通过反射修改私有的一般字段,常量,只读字段,静态字段,静态只读字段并打印出结果。在这里可以可以看出反射是很强大的,既然可以修改私有的只读字段,但要注意实际上通过反射也无法修改常量,修改会报错,这与常量的存储方式有关。通过反射给人一种可以跳过验证规则的假象。示例代码:

通过反射修改私有字段
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace FalseVerification
{
    
class Program
    {
        
static void Main(string[] args)
        {
            Type type 
= typeof(SomeType);

            SomeType st 
= new SomeType();
            st.Print();

            
// 正常字段,当然可以修改
            FieldInfo fi = type.GetField("f1", BindingFlags.NonPublic | BindingFlags.Instance);
            fi.SetValue(st, (Int32)fi.GetValue(st) 
+ 1);

            
// 常量字段,反射也无法修改,如果取消下面语句的注释,执行会出错
            /* 原因说明:常量的值必须在编译时就确定(只能是基元类型),也就是说在定义时就赋值。
               编译后常量的值是保存在程序集的元数据中,在运行时是不可修改的;
               而其它字段是存储在动态内存中,在运行时是可修改的。
*/
            fi 
= type.GetField("f2", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
            
//fi.SetValue(null, (Int32)fi.GetValue(null) + 1);

            
// 只读字段,可通过反射方式修改值
            fi = type.GetField("f3", BindingFlags.NonPublic | BindingFlags.Instance);
            fi.SetValue(st, (Int32)fi.GetValue(st) 
+ 1);

            
// 静态字段,也可修改
            fi = type.GetField("f4", BindingFlags.NonPublic | BindingFlags.Static);
            fi.SetValue(
null, (Int32)fi.GetValue(null+ 1);

            
// 静态只读字段,下面代码不出错,改变了反射字段的值,但类中的字段值并没有被改变
            fi = type.GetField("f5", BindingFlags.NonPublic | BindingFlags.Static);
            fi.SetValue(
null, (Int32)fi.GetValue(null+ 1);
            Int32 f5 
= (Int32)fi.GetValue(null); // i5 得到值为51

            st.Print();
            Console.WriteLine(
"f5: " + f5.ToString());
            Console.ReadKey();
        }
    }

    
public class SomeType
    {
        
private Int32 f1 = 30;// 私有字段
        private const Int32 f2 = 10;// 私有常量字段
        private readonly Int32 f3 = 20;// 私有只读字段
        private static Int32 f4 = 40;// 私有静态字段
        private static readonly Int32 f5 = 50;// 私有静态只读字段

        
public void Print()
        {
            Console.WriteLine(
"f1: " + f1.ToString());
            Console.WriteLine(
"f2: " + f2.ToString());
            Console.WriteLine(
"f3: " + f3.ToString());
            Console.WriteLine(
"f4: " + SomeType.f4.ToString());
            Console.WriteLine(
"f5: " + SomeType.f5.ToString());

            Console.WriteLine();
        }
    }
}

 运行结果:

  从这个结果可以看出,通过反射可以修改私有的普通字段,可以修改只读字段,可以修改静态字段,修改这三个都没有问题,但是不可以修改常量字段。当通过反射修改静态只读字段时(有点类似常量)时,可以正常执行,但是通过类获取的静态只读字段并没有改变,说明实际并没有修改成功,我们实际修改的只是通过反射获取的静态只读字段,并没有实际映射回去。具体原因感兴趣的同学可以通过IL分析一下,鄙人不才,不会IL。

  第二部分:GC中的GCBeep示例

  先看看代码:

GCBeep
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace GCBeep
{
    
class Program
    {
        
static void Main(string[] args)
        {
            
new GCBeep();

            
// 创建很多对象,让GC执行垃圾收集
            for (Int32 x = 0; x < 10000; x++)
            {
                Console.WriteLine(x);
                Byte[] b 
= new Byte[100];
            }
        }
    }

    
internal sealed class GCBeep
    {
        
/* 
         这是一个Finalize方法,GC在发现根
         中没有对于GCBeep对象的引用时会在
         垃圾收集时调用这个方法
        
*/
        
~GCBeep()
        {
            Console.Beep();
// 让控制台发出报警的声音

            
if (!AppDomain.CurrentDomain.IsFinalizingForUnload() && !Environment.HasShutdownStarted)
            {
                
// 如果不是在应用程序域卸载或应用程序自行关闭时,新建一个GCBeep对象并没有变量引用它
                new GCBeep();
            }
        }
    }
}

  GC在垃圾收集时会执行对象的Finalize方法来释放非托管的资源,在这里也就是~GCBeep(),在~GCBeep()方法内部首先让控制台发出报警声,之后会在不是应用程序域卸载和应用程序主动退出的情况下再创建一个GCBeep对象,并不让任何变量引用它,此时这个对象实际上已经成为下次GC执行垃圾收集的目标。在主函数中同样也是创建一个无变量引用的GCBeep对象作为程序的入口点,之后创建大量的对象,当托管堆的相关内存用完之后垃圾收集就会执行来回收垃圾释放内存以便容纳新创建的对象,这时垃圾收集调用~GCBeep()发出报警声。创建10000个Byte[] b = new Byte[100];在我的x86的win7笔记本上发出两次报警声,大家可以修改10000到更大测试,来根据报警声来判断执行~GCBeep()的次数。

  这个例子我觉得设计的很精美,也很简洁,更重要的是可以说明垃圾收集时确实自动执行了对象的Finalize方法来释放非托管资源(作为示例这里并没有实际释放非托管资源)。