资源管理(尤其是内存回收)曾经是程序员的噩梦,不过在.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. Python3-for-enumerate

    languages = ["C", "C++", "Perl", "Python"] for x in language ...

  2. 多项式乘法(FFT)模板 && 快速数论变换(NTT)

    具体步骤: 1.补0:在两个多项式最前面补0,得到两个 $2n$ 次多项式,设系数向量分别为 $v_1$ 和 $v_2$. 2.求值:用FFT计算 $f_1 = DFT(v_1)$ 和 $f_2=DF ...

  3. 为什么说Redis是单线程的以及Redis为什么这么快!(转)

    一.前言 近乎所有与Java相关的面试都会问到缓存的问题,基础一点的会问到什么是“二八定律”.什么是“热数据和冷数据”,复杂一点的会问到缓存雪崩.缓存穿透.缓存预热.缓存更新.缓存降级等问题,这些看似 ...

  4. nginx+tomcat遇到的https重定向到http问题

    nginx做反向代理时,需要把请求头信息一起发送给tomcat,不然tomcat中的域名绑定就无法发挥作用了. 今天又遇到https请求被拦截器重定向到登陆页居然变成http的问题,导致小程序无法访问 ...

  5. am335x system upgarde ddr3 capacity configuration base on TI DDR3 Software Leveling Tool (二十)

    follow test is  use ti DDR Software Leveling op log. AM335x DDR3 Software Leveling -- Version: Beta ...

  6. 51 Nod 1475 建设国家 (优先队列+贪心)

    1475 建设国家  基准时间限制:1 秒 空间限制:131072 KB 分值: 20 难度:3级算法题  收藏  关注 小C现在想建设一个国家.这个国家中有一个首都,然后有若干个中间站,还有若干个城 ...

  7. Linux 文件查看

    链接:https://www.nowcoder.com/questionTerminal/fb39fbeec71f43a3a16edeb0bc98f4ac 来源:牛客网 /var/log/messag ...

  8. CSP-S 模拟测试 45 题解

    由于咕掉的题解太多了,所以只能趁改完不动题的时间,来补补坑qwq,还是太弱了. 考试过程: 到新机房的第一次考试,貌似海星? 第一题一开始就觉得是个贪心,但以为所有小怪兽都要打完,所以想复杂了,但后来 ...

  9. 【CUDA 基础】5.2 共享内存的数据布局

    title: [CUDA 基础]5.2 共享内存的数据布局 categories: - CUDA - Freshman tags: - 行主序 - 列主序 toc: true date: 2018-0 ...

  10. 2019ICPC上海网络赛 A Lightning Routing I 点分树(动态点分治)+线段树

    题意 给一颗带边权的树,有两种操作 \(C~e_i~w_i\),将第\(e_i\)条边的边权改为\(w_i\). \(Q~v_i\),询问距\(v_i\)点最远的点的距离. 分析 官方题解做法:动态维 ...