漫谈Object基类(二)

前言

     上一篇笔者已经讲述了Object中有关ToString()、Equals()、GetHashCode()方法的基本运用,本章将简单介绍另外三个方法:GetType()、MemberwiseClone()、Finalize(),希望在本章完了以后大家对Object对象有一个深刻详细的认识。

应用

4 GetType(): 获取元数据

     提到GetType()方法,就不得不引入反射。在.NET中,反射是一个运行库类型发现的过程。通过反射,可以得到一个给定程序集所包含的所有类型的列表,这个列表包括给定类型中定义的方法、字段、属性和事件。也可以动态地发现给定类(或结构)支持的接口、方法的参数和其他相关细节(基类、命名空间、清单数据等)。对于反射,这里就不多做介绍。GetType()方法,返回的是一个System.Type类型,一旦获取了类型的Type信息(实质上是类型元数据)我们就相当于得到了该类型的所有信息,包括接口定义,方法参数等等。一般来说有三种方式得到类型的Type信息:

  1. 使用System.Object定义的GetType()方法,它返回一个表示当前对象元数据的Type信息。要使用这个方法,必须得到类型的编译时信息,并且需要先建立一个实例。
  2. 使用System.Type.GetType()得到Type引用,里面包含三个非常有用的静态函数,可通过传入类型的完全限定名来获取Type信息。
  3. 最后一个获取类型信息的方法是使用C# typeof运算符运算符。类似Type.GetType(),使用typeof运算符,我们不需要先建立一个实例来提取类型信息。但是仍然需要知道类型的编译时信息。

     第一种方法的局限性在于必须先创建类型的实例才能提取类型信息,也就是说类型是在编译时检查的;第三种方法,同样需要知道类型的编译时信息;第二种相对来说最灵活,只需要通过传入类型完全限定名的字符串就能提取类型信息。本文旨在讨论Object的GetType方法,下面给出一段代码示例:

Code

     代码依旧是在之前的基础上改动,前面已经演示了ToString()方法,只不过现在我们是通过反射来调用ToString()方法,运行前需要引用System.Reflection命名空间,代码比较简单,这里就不作解释了,运行结果如下:

     ErrorCode = 1;ErrorString = error string

     System.Type类定义了大量成员,可以用来检查一个类型的元数据,调用属性、方法,获取Attribute信息等,大家可以查API熟悉一下,只要记住一点:反射的实质是获取元数据。

5 MemberwiseClone(): 浅复制

     MemberwiseClone()方法返回一个新的对象,它是当前对象的逐个成员的副本。因此,如果你的对象包含到其他对象的引用,那么到这些类型的引用将被复制(也就是说,它实现了浅复制)。如果对象只包含值类型,得到的是值的完全副本。需要注意的是,MemberwiseClone方法是proteced,故对象的用户无法直接调用这个方法,而一个对象可能在克隆过程中自己调用这个方法。如果我们在Main()方法中写类似的代码:Status status = new Status(1, "error string"); Status newStatus = (Status )status.MemberwiseClone(); 是不能通过编译的,因为由于MemberwiseClone()方法的访问级别限制,你只能在Status类里面通过this.MemberwiseClone()调用该方法,而不能在客户代码里面直接调用。

     一般来说,我们主要用它来实现Clone方法,如果你想使自己的自定义类型支持向调用方法返回自身同样副本的能力,需要实现标准ICloneable接口。这个类型定义了一个简单的方法Clone(),接口定义如下:

ICloneable

     很明显,不同对象的Clone()方法实现不一样。但基本功能差不多,都是将成员变量的值复制到新的对象实例,然后向用户返回该实例。现在我们对Status类实现ICloneable接口,实现代码如下:

Clone

     当然,有时候为了简单起见,我们可以直接调用MemberwiseClone()方法来完成这样的功能,代码示例如下:

Clone

     很显然,这样更简单,但是有个前提:克隆对象包含的类型都是值类型,如果里面包含引用类型,由于MemberwiseClone()是浅复制,引用类型只会复制对象的引用。有个折中的方式是,我们可以先调用this.MemberwiseClone()方法克隆值类型,然后通过new创建引用类型对象。假设Status类里面还有个StateDescription的引用类型变量,那么我们要想实现深度复制,就得这样写代码了:

Code

 6 Finalize(): 资源清理

     当为自定义类重写Finalize()时,就建立了一个地方,存放为类型执行必要的清理逻辑。因为这个成员被定义为受保护的,所以不可能直接调用一个对象的Finalize()方法。相反,在从内存删除这个对象之前,垃圾回收器会调用对象的Finalize()方法。

     当然,Finalize()的调用将(最终)发生在一次自然的垃圾回收或用程序通过GC.Collect()强制回收过程中。另外,当承载应用程序的应用程序域从内存中卸载时,会自动调用类型的终结器方法。大多数的C#类都不需要显示的清理逻辑。原因很简单:如果类型使用了其他托管对象,一切都最终会被垃圾回收。只是在你使用非托管资源时,才可能需要设计一个在用完后清理自身的类。需要注意的是,在结构类型上重写Finalize()是不合法的。这一点非常重要,因为结构是值类型,它们本来就从不分配在堆上。

     在C#中,不能通过override关键字重写Finalize()方法,当想配置自定义的C#类类型来重写Finalize()方法时,可以使用下面的析构函数语法来达到同样的效果。之所以要用这种重写虚函数的替代形式,是因为当C#编译器执行一个构造函数时,它将自动在Finalize()方法中增加许多必需的基础代码。假设需要在Status类中重写Finalize()方法,代码如下:

 ~Status()
 {
    
//清除这里非托管的资源
 }       

     要记住,Finalize()方法的作用是保证.NET对象能在垃圾回收时清除非托管资源。如果创建了一个不使用非托管实体的类型,终结是没有用的。事实上,只要有可能的话,就应该在设计类型时避免提供Finalize()方法,原因很简单,终结是要花费时间的。

     当在托管堆上分配对象时,运行库自动确定该对象是否提供一个自定义的Finalize()方法。如果是这样,对象将被标记为可终结的,同时一个指向这个对象的指针被保存在名为终结队列的内部队列中。终结队列是一个由垃圾回收器维护的表,它指向每一个在从堆上删除之前必须被终结的对象。当垃圾回收器确定到了从内存中释放一个对象的时间时,它检查终结队列上的每一个项,并将对象从堆上复制到另一个称作终结可达表的托管结构上。此时,下一个垃圾回收时将产生另一个线程,为每一个在可达表中的对象调用Finalize()方法。因此,为了真正终结一个对象,至少要进行两次垃圾回收。总而言之,尽管对象的终结能够保证对象可以清除非托管的资源,但它本质上仍然是非确定的,而且由于额外的幕后处理,速度会变的很慢。

     除了重写Finalize()方法,其实我们也可以通过另一个更安全的方式处理对象清理工作,即通过实现IDisposable接口,它定义了一个名为Dispose()的方法:

IDisposable

     如果提供IDisposable接口,就是假设当对象的用户不再使用这个对象时,会在这个对象引用离开作用域之前手动调用Dispose()。这样,对象可以执行任何必要的非托管资源的清理,而且不会再有将对象放在终结队列上导致的性能损失,也不必等待垃圾回收器触发类的终结逻辑。需要注意的是,Dispose()方法不只负责释放一个对象的托管资源,还应该对任何它包含的可处置对象调用Dispose()。于Finalize()不一样,在Dispose()方法中与其他托管对象通信是安全的(在Finalize()方法中调用托管对象的话,难以预见该托管对象是否已经被垃圾回收器清理了,固会产生难以预见的情况)。原因很简单:当对象的用户调用这个方法时,对象仍然在托管堆上,并可以访问所有其他分配在堆上的对象。

     可能有人要说,虽然我实现了IDisposable接口,但是如果程序中忘记了调用Dispose()怎么办呢?答案是混合两种模式。微软定义了一个正式的可处置模式,它在健壮性、可维护性和性能三者间取得了平衡,具体实现如下:

Code

     代码示例比较长,但是建议大家仔细看一下,是个很优雅的设计。

总结

     至此,对System.Object的分析就到这了,本文更多的是从应用的层面探讨Object基类,其中又引入了很多与之相关的应用点,如果你完整地阅读了这两篇文章,我对你的耐心表示感谢,也希望你能从中有所收获。

posted @ 2008-12-05 18:19  lemonade  阅读(361)  评论(0编辑  收藏  举报