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

![](https://img2018.cnblogs.com/blog/710776/201906/710776-20190613112138058-2026075746.png)

资源管理

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

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

  • 托管资源 由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模式中,我们注意到一个以~开头的方法,如下所示:

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

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

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

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

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

注意2 如果调用者已经调用Dispose方法进行了显式地资源释放,那么,隐式释放资源(也就是终结器)就没有必要再运行了。

FCL中的类型GC提供了静态方法SuppressFinalize来通知垃圾回收器这一点。注意查看Dispose方法:

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#规范整理·泛型委托事件》也可以看看!

C#规范整理·资源管理和序列化的更多相关文章

  1. .NET代码编写规范 整理

    .NET代码编写规范 整理 .NET代码编写规范 - [ASP.NET] 2009-02-26 | Tag: 版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明 http://lensp ...

  2. 我自己总结的C#开发命名规范整理了一份

    我自己总结的C#开发命名规范整理了一份 标签: 开发规范文档标准语言 2014-06-27 22:58 3165人阅读 评论(1) 收藏 举报  分类: C#(39)  版权声明:本文为博主原创文章, ...

  3. C#规范整理·泛型委托事件

    基于泛型,我们得以将类型参数化,以便更大范围地进行代码复用.同时,它减少了泛型类及泛型方法中的转型,确保了类型安全.委托本身是一种引用类型,它保存的也是托管堆中对象的引用,只不过这个引用比较特殊,它是 ...

  4. C#规范整理·集合和Linq

    LINQ(Language Integrated Query,语言集成查询)提供了类似于SQL的语法,能对集合进行遍历.筛选和投影.一旦掌握了LINQ,你就会发现在开发中再也离不开它.   开始! 前 ...

  5. JAVA基础4---序列化和反序列化深入整理(JDK序列化)

    一.什么是序列化和反序列化? 序列化:将对象状态信息转化成可以存储或传输的形式的过程(Java中就是将对象转化成字节序列的过程) 反序列化:从存储文件中恢复对象的过程(Java中就是通过字节序列转化成 ...

  6. Rocket - spec - RISC-V规范整理

    https://mp.weixin.qq.com/s/xP8JRhkmgUQf0QRm3S2mjA   根据RISC-V规范整理的几个文档.   ​​     1. 原文链接 https://risc ...

  7. HTTP API响应数据规范整理

    概述 本文档为本人对长期开发API接口所整理的经验总结,如有不完善或不合理的地方,望各位多提意见. 文档目的为规范服务器端API接口,便于服务器端与客户端代码重用.服务器端和客户端可根据实际所定义规范 ...

  8. 前端CSS规范整理_转载、、、

    一.文件规范 1.文件均归档至约定的目录中. 具体要求通过豆瓣的CSS规范进行讲解: 所有的CSS分为两大类:通用类和业务类.通用的CSS文件,放在如下目录中: 基本样式库 /css/core 通用U ...

  9. PHP编码规范整理,很全很实用(图文版)

    有一个组织叫做“php互操作性框架制定小组”,这个小组的主要目的是制定各种PHP编码规范的,下面就是我根据小组提供的建议整理的一些常用的编码规范. PSR-1: 1.PHP代码文件必须以<?ph ...

随机推荐

  1. 清除文本中Html的标签

    /// <summary> /// 清除文本中Html的标签 /// </summary> /// <param name="Content"> ...

  2. pycharm mysql数据源配置、SQL方言配置

    会发现有提示,看着不爽,但不影响运行程序, 这里提示没有配置数据源,现在配置MYSQL数据源 然后看到右边Database选项卡,点击 然后可能会出现网络防火墙提示,选择全部允许,之后可能会在pych ...

  3. vue中前进刷新、后退缓存用户浏览数据和浏览位置的实践

    vue中前进刷新.后退缓存用户浏览数据和浏览位置的实践 2018年07月07日 11:58:40 大灰狼的小绵羊哥哥 阅读数:4492   vue中,我们所要实现的一个场景就是: 1.搜索页面==&g ...

  4. 简单NLT

    目录 操作文本 需求 代码实现 操作文本 I have a dream that my four little children will one day live in a nation where ...

  5. golang 文件导入数据追加sheet

    func ReadXlsx(c []CmdbTest, SheetName string) error {     //打开文件,如果文件不存在创建,存在就打开     path := ". ...

  6. python之json读写

    #将字典转json并写入文件 import json i=3 j=5 a={'a':i,'b':j} js=json.dumps(a) print(js) with open("/Users ...

  7. Linux设备驱动程序 之 字符设备的注册

    内核内部使用struct cdev结构来标识字符设备,在内核调用设备的操作之前,必须分配并注册一个或者多个上述结构,为此,我们的代码需要包含<linux/cdev.h>,其中定义了这个结构 ...

  8. .net reflector

    https://www.red-gate.com/dynamic/products/dotnet-development/reflector/download https://github.com/s ...

  9. MySQL Cluster 集群部署

    前言 此篇博客用以介绍 MySQL Cluster 集群部署方法 一.节点规划 序号 IP地址 节点名称 1 172.16.1.201 mysql-manage 2 172.16.1.202 mysq ...

  10. nginx详解(代理服务器的解释+nginx 在linux 下的安装+nginx.conf 中的配置解释)

    一.概论 1.什么是代理服务器 代理服务器,客户机在发送请求时,不会直接发送给目的主机,而是先发送给代理服务器,代理服务接受客户机请求之后,再向主机发出,并接收目的主机返回的数据,存放在代理服务器的硬 ...