C#规范整理·资源管理和序列化

资源管理(尤为是内存回收)曾经是程序员的噩梦,不过在.NET平台上这个噩梦彷佛已经不复存在。CLR在后台为垃圾回收作了不少事情,使得咱们如今谈起在.NET上进行开发时,都会说仍是new一个对象吧!回收?有垃圾回收器呢。其实并无这么简单。
  对象序列化是现代软件开发中的一项重要技术,不管是本地存储仍是远程传输,都会使用序列化技术来保持对象状态。


资源管理

1.显式释放资源需继承接口IDisposable

C#中的每个类型都表明一种资源,而资源又分为两类:html

  • 托管资源 由CLR管理分配和释放的资源,即从CLR里new出来的对象。
  • 非托管资源 不受CLR管理的对象,如Windows内核对象,或者文件、数据库链接、套接字、COM对象等。

若是咱们的类型使用到了非托管资源,或者须要显式地释放托管资源,那么就须要让类型继承接口IDisposable,这毫无例外。这至关于告诉调用者:类型对象是须要显式释放资源的,你须要调用类型的Dispose方法。,一个标准的继承了IDisposable接口的类型应该像下面这样去实现。这种实现咱们称为Dispose模式:程序员

public class SampleClass:IDisposable
{
 //演示建立一个非托管资源   
 private IntPtr nativeResource=Marshal.AllocHGlobal(100);    

//演示建立一个托管资源  
 private AnotherResource managedResource=new AnotherResource();  
 private bool disposed=false;   

 ///<summary>   
 ///实现IDisposable中的Dispose方法    
///</summary>    
 public void Dispose()    
 {       
 //必须为true      
  Dispose(true);        
//通知垃圾回收机制再也不调用终结器(析构器)      
  GC.SuppressFinalize(this);   
 }    

///<summary>   
///不是必要的,提供一个Close方法仅仅是为了更符合其余语言(如C++)的规范    
///</summary>   
 public void Close()    
 {       
 Dispose();    
 }    
///<summary>  

  ///必需的,防止程序员忘记了显式调用Dispose方法    
///</summary>    
~SampleClass()
  {      
  //必须为false       
 Dispose(false);    
  }   

 ///<summary>    
///非密封类修饰用protected virtual    
///密封类修饰用private    
///</summary>   
 ///<param name="disposing"></param>  
  protected virtual void Dispose(bool disposing) 
   {       
       if(disposed)       
       {          
          return;     
       }      
 
     if(disposing)   
      {          
  //清理托管资源    
        if(managedResource!=null)      
      {             
   managedResource.Dispose();        
        managedResource=null;     
      }      
  }        

//清理非托管资源       
 if(nativeResource!=IntPtr.Zero)   
     {          
  Marshal.FreeHGlobal(nativeResource);      
  nativeResource=IntPtr.Zero;
    }      
  //让类型知道本身已经被释放    
    disposed=true;   
 }  

  public void SamplePublicMethod()  
  {      
     if(disposed)     
     {        
       throw new ObjectDisposedException("SampleClass","SampleClass is   disposed");     
     }     
   //省略   
  }
}

若是类型须要显式释放资源,那么必定要继承IDispose接口。
承IDispose接口也为实现语法糖using带来了便利。在C#编码中,若是像下面这样使用using,编译器会自动为咱们生成调用Dispose方法的IL代码:数据库

using(SampleClass c1=new SampleClass())
{    
   //省略
}

至关于网络

SampleClass c1;
try{   
    c1=new SampleClass();   
   //省略
   }

 finally
  {    
   c1.Dispose();
  }

2.即便提供了显式释放方法,也应该在终结器中提供隐式清理

在标准的Dispose模式中,咱们注意到一个以~开头的方法,以下所示:this

///<summary>
///必须,防止程序员忘记了显式调用Dispose方法
///</summary>

~SampleClass()
{    
   //必须为false   
 Dispose(false);
}

这个方法叫作类型的终结器。提供终结器的意义在于:咱们不能奢望类型的调用者确定会主动调用Dispose方法,基于终结器会被垃圾回收器调用这个特色,它被用做资源释放的补救措。编码

对于没有继承IDisposable接口的类型对象,垃圾回收器则会直接释放对象所占用的内存;而对于实现了Dispose模式的类型,在每次建立对象的时候,CLR都会将该对象的一个指针放到终结列表中,垃圾回收器在回收该对象的内存前,会首先将终结列表中的指针放到一个freachable队列中。同时,CLR还会分配专门的线程读取freachable队列,并调用对象的终结器,只有到这个时候,对象才会真正被标识为垃圾,而且在下一次进行垃圾回收时释放对象占用的内存。线程

能够看到,实现了Dispose模式的类型对象,起码要通过两次垃圾回收才能真正地被回收掉,由于垃圾回收机制会首先安排CLR调用终结器。基于这个特色,若是咱们的类型提供了显式释放的方法来减小一次垃圾回收,同时也能够在终结器中提供隐式清理,以免调用者忘记调用该方法而带来的资源泄漏。设计

注意1 在有的文档中,终结器也称作析构器。指针

注意2 若是调用者已经调用Dispose方法进行了显式地资源释放,那么,隐式释放资源(也就是终结器)就没有必要再运行了。
FCL中的类型GC提供了静态方法SuppressFinalize来通知垃圾回收器这一点。注意查看Dispose方法:code

public void Dispose()
{   
    Dispose(true); 
   GC.SuppressFinalize(this);
}

3.Dispose方法应容许被屡次调用

一个类型的Dispose方法应该容许被屡次调用而不抛异常。鉴于这个缘由,类型内部维护了一个私有的布尔型变量disposed,以下所示:

private bool disposed=false;

在实际清理代码的方法中,加入了以下的判断语句:

if(disposed)
{    
return;
}

在//省略部分的代码,方法的最后为disposed赋值为true:disposed=true;这意味着若是类型已经被清理过一次,那么清理工做将再也不进行。对象被调用过Dispose方法,并不表示该对象已经被置为null,且被垃圾回收机制回收过内存,已经完全不存在了。事实上,对象的引用可能还在。可是,对象被Dispose过,说明对象的正常状态已经不存在了,此时若是调用对象公开的方法,应该会为调用者抛出一个ObjectDisposedException。

4.在Dispose模式中应提取一个受保护的虚方法

真正实现IDisposable接口的Dispose方法并无作实际的清理工做,它实际上是调用了下面这个带布尔参数且受保护的虚方法:

///<summary>
///非密封类修饰用protected virtual
///密封类修饰用private///</summary>
///<param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{        
//省略代码
}

之因此提供这样一个受保护的虚方法,是由于考虑了这个类型会被其余类继承的状况。若是类型存在一个子类,子类也许会实现本身的Dispose模式。受保护的虚方法用来提醒子类:必须在实现本身的清理方法时注意到父类的清理工做,即子类须要在本身的释放方法中调用base.Dispose方法。

若是不为类型提供这个受保护的虚方法,颇有可能让开发者设计子类的时候忽略掉父类的清理工做。因此,基于继承体系的缘由,要为类型的Dispose模式提供一个受保护的虚方法。

5.在Dispose模式中应区别对待托管资源和非托管资源

Dispose模式设计的思路基于:若是调用者显式调用了Dispose方法,那么类型就该循序渐进地将本身的资源所有释放。若是调用者忘记调用Dispose方法了,那么类型就假定本身的全部托管资源会所有交给垃圾回收器回收,因此不进行手工清理。理解了这一点,咱们就理解了为何在Dispose方法中,虚方法传入的参数是true,而在终结器中,虚方法传入的参数是false。

6.具备可释放字段的类型或拥有本机资源的类型应该是可释放的

咱们将C#中的类型分为:普通类型和继承了IDisposable接口的非普通类型。非普通类型除了那些包含托管资源的类型外,还包括类型自己也包含一个非普通类型的字段的类型。
在标准的Dispose模式中,咱们对非普通类型举了一个例子:一个非普通类型AnotherResource。因为AnotherResource是一个非普通类型,因此若是如今有这么一个类型,它组合了AnotherResource,那么它就应该继承IDisposable接口,代码以下所示:

class AnotherSampleClass:IDisposable
{    

private AnotherResource managedResource=new AnotherResource();    
private bool disposed=false;   
 public void Dispose()   
 {        
   Dispose(true);  
   GC.SuppressFinalize(this);   
 }
}

类型AnotherSampleClass虽然没有包含任何显式的非托管资源,可是因为它自己包含了一个非普通类型,因此咱们仍旧必须为它实现一个标准的Dispose模式。
除此之外,类型拥有本机资源(即非托管类型资源),它也应该继承IDisposable接口。

7.及时释放资源

不少人会注意到:垃圾回收机制自动为咱们隐式地回收了资源(垃圾回收器会自动调用终结器),因而不由会问:为何还要主动释放资源呢?咱们来看如下这个例子:

private void buttonOpen_Click(object sender,EventArgs e)
{    
    FileStream fileStream=new FileStream(@"c:\test.txt",FileMode.Open);
}

private void buttonGC_Click(object sender,EventArgs e)
{   
    System.GC.Collect();
}

若是连续两次单击打开文件按钮,系统就会报错,以下所示:
IOException:文件"c:\test.txt" 正由另外一进程使用,所以该进程没法访问此文件。

如今来分析:在打开文件的方法中,方法执行完毕后,因为局部变量fileStream在程序中已经没有任何地方引用了,因此它会在下一次垃圾回收时被运行时标记为垃圾。那么,何时会进行下一次垃圾回收呢,或者说垃圾回收器何时才开始真正进行回收工做呢?微软官方的解释是,当知足如下条件之一时将发生垃圾回收:

  • 系统具备低的物理内存。
  • 由托管堆上已分配的对象使用的内存超出了可接受的范围。
  • 调用GC.Collect方法。几乎在全部状况下,咱们都没必要调用此方法,由于垃圾回收器会负责调用它。

但在本实例中,为了体会一下不及时回收资源的危害,因此进行了一次GC.Collect方法的调用,你们能够仔细体会运行这个方法所带来的不一样。

垃圾回收机制中还有一个“代”的概念。一共分为3代:0代、1代、2代。第0代包含一些短时间生存的对象,如示例代码中的局部变量fileStream就是一个短时间生存对象。当buttonOpen_Click退出时,fileStream就被丢到了第0代,但此刻并不进行垃圾回收,当第0代满了的时候,运行时会认为如今低内存的条件已知足,那时才会进行垃圾回收。因此,咱们永远不知道fileStream这个对象(或者说资源)何时才会被回收。在回收以前,它实际已经没有用处,却始终占据着内存(或者说资源)不放,这对应用系统来讲是一种极大的浪费,而且,这种浪费还会干扰程序的正常运行(如在本实例中,因为它始终占着文件资源,致使咱们不能再次使用这个文件资源了)。

不及时释放资源还带来另一个问题。在上面中咱们已经了解到,若是类型自己继承了IDisposable接口,垃圾回收机制虽然会自动帮咱们释放资源,可是这个过程却延长了,由于它不是在一次回收中完成全部的清理工做。本实例中的代码由于fileStream继承了IDisposable接口,故第一次进行垃圾回收的时候,垃圾回收器会调用fileStream的终结器,而后等待下一次的垃圾回收,这时fileStream对象才有可能被真正的回收掉。

了解了不及时释放资源的危害后,如今来改进这个程序,以下所示:

private void buttonOpen_Click(object sender,EventArgs e)
{  
  FileStream fileStream=new FileStream(@"c:\test.txt",FileMode.Open);  
  fileStream.Dispose();
}

这确实是一种改进,可是咱们没考虑到方法中的第一行代码可能会抛出异常。若是它抛出异常,那么fileStream.Dispose()将永远不会执行。因而,再一次改进,以下所示:

FileStream fileStream=null;
try
{
 fileStream=new FileStream(@"c:\test.txt",FileMode.Open);
}
finally
{    
fileStream.Dispose();
}

为了更进一步简化语句,还可使用语法糖“using”关键字。

8.必要时应将再也不使用的对象引用赋值为null

在CLR托管的应用程序中,存在一个“根”的概念,类型的静态字段、方法参数,以及局部变量均可以做为“根”存在(值类型不能做为“根”,只有引用类型的指针才能做为“根”)。
当检查到方法内的“根”时,若是发现没有任何一个地方引用了局部变量,则无论是否已经显式将其赋值为null,都意味着该“根”已经被中止。而后,垃圾回收器会发现该根的引用为空,同时标记该根可被释放。

须要注意一下几点

  1. 局部变量赋值为null无心义,由于编译器在编译时就会过滤。
  2. 类型的静态字段赋值为null是有意义的。是由于类型的静态字段一旦被建立,该“根”就一直存在。因此,垃圾回收器始终不会认为它是一个垃圾。非静态字段则不存在这个问题。

在实际工做中,一旦咱们感受到本身的静态引用类型参数占用的内存空间比较大,而且用完后不会再使用,即可以马上将其赋值为null。这也许并没必要要,但这绝对是一个好习惯。试想在一个系统中那些时不时在类型中出现的静态变量吧!它们就那样静静地待在内存里,一旦被建立,就永远不会离开。或许咱们能够专门为此写一个小建议,那就是:尽可能少用静态变量。

序列化

1.为无用字段标注不可序列化

序列化是指这样一种技术:把对象转变成流。相反的过程,咱们称为反序列化。在不少的场合都须要用到这项技术,例如:

  • 把对象保存到本地,在下次运行程序的时候,恢复这个对象。
  • 把对象传到网络中的另一台终端上,而后在此终端还原这个对象。
  • 其余的场合,如:把对象复制到系统的粘贴板中,而后用快捷键Ctrl+V恢复这个对象。

有如下几方面的缘由,决定了要为无用字段标注不可序列化:

  1. 节省空间。类型在序列化后每每会存储到某个地方,如数据库、硬盘或内存中,若是一个字段在反序列化后不须要保持状态,那它就不该该被序列化,这会占用宝贵的空间资源。
  2. 反序列化后字段信息已经没有意义了。如Windows内核句柄,在反序列化后每每已经失去了意义,因此它就不该该被序列化。
  3. 字段由于业务上的缘由不容许被序列化。例如,明文密码不该该被序列化后一同保存在文件中。
  4. 若是字段自己所对应的类型在代码中未被设定为可序列化,那它就该被标注不可序列化,不然运行时会抛出异常SerializationException。
[Serializable]
class Person
{   

 [NonSerialized]    
 private decimal salary;
  public decimal Salary
  {        
     get   { return salary;  }        
     set   {  salary=value;  }   
  }      

 private string name;    
 public int Age{get;set;}  
 public string Name   
 {        
     get   { return name;  }        
     set  {  name=value;     
 }    

  [field:NonSerialized]    
  public event EventHandler NameChanged;
}

注意
1.因为属性本质上是方法,因此不能将NonSerialized特性应用于属性上,在标识某个属性不能被序列化时,自动实现的属性显然已经不能使用。
2.要让事件不能被序列化,需使用改进的特性语法field:NonSerialized。

2.利用定制特性减小可序列化的字段

特性(attribute)能够声明式地为代码中的目标元素添加注解。运行时能够经过查询这些托管模块中的元数据信息,达到改变目标元素运行时行为的目的。在System.Runtime.Serialization命名空间下,有4个这样的特性,下面是MSDN上对它们的解释:

  • OnDeserializedAttribute,当它应用于某方法时,会指定在对象反序列化后当即调用此方法。
  • OnDeserializingAttribute,当它应用于某方法时,会指定在反序列化对象时调用此方法。
  • OnSerializedAttribute,若是将对象图应用于某方法,则应指定在序列化该对象图后是否调用该方法。
  • OnSerializingAttribute,当它应用于某个方法时,会指定在对象序列化前调用此方法。

示例:

[Serializable]
class Person
{    
public string FirstName;    
public string LastName;    

[NonSerialized]    
public string ChineseName;   

[OnDeserializedAttribute]    
void OnSerialized(StreamingContext context) 
   {        
        ChineseName=string.Format("{0}{1}",LastName,FirstName);  
  }
}

3.使用继承ISerializable接口更灵活地控制序列化过程

除了利用特性Serializable以外,咱们还能够注意到在序列化的应用中,经常会出现一个接口ISerializable。接口ISerializable的意义在于,若是特性Serializable,以及与其相配套的OnDeserializedAttribute、OnDeserializingAttribute、OnSerializedAttribute、OnSerializingAttribute、NonSerialized等特性不能彻底知足自定义序列化的要求,那就须要继承ISerializable了。
例如咱们要将一个对象反序列化成为另一个对象,就要都实现ISerializable接口,原理其实很简单,那就是在一个对象的GetObjectData方法中处理序列化,在另外一个对象的受保护构造方法中反序列化。

4.实现ISerializable的子类型应负责父类的序列化

咱们将要实现的继承自ISerializable的类型Employee有一个父类Person,假设Person没有实现序列化,而如今子类Employee却要求可以知足序列化的场景。不过很遗憾,序列化器没有默认去处理Person类型对象,须要咱们在子类中受保护的构造方法和GetObjectData方法,为它们加入父类字段的处理

总结

若有须要, 上一篇的《C#规范整理·泛型委托事件》也能够看看!

相关文章
相关标签/搜索