C#采用GC(垃圾回收器)来管理内存,GC在它独自的线程上运行。但是GC只管理内存,而不会翻译其它的资源。在C++的时候,我们可以在析构函数里面来释放资源,但在C#中,因为我们没有办法确定对象是什么时候回收的,其析构函数的调用时间并不是可预期的,因此在析构函数里面翻译资源并不是一个很好的办法。
另外,如果我们在析构函数里面回收资源,由于GC在回收内存前必须调用其析构函数,因此GC必须另起一个线程来调用析构函数,该对象占用的内在也只能在“下一次”GC回收内存的时候被回收。之所以用“下一次”,是因为其并不一定是下一次,很有可能是很久以后。
这涉及到GC回收内存的时候一个机制:.NET GC定义了代来优化资源回收。“代”表示了对象有可能成为垃圾的程度。刚创建的对象是0代,经过一次垃圾回收的是1代,经过两次垃圾回收的是2代。0代的对象最有可能是垃圾,因为是刚创建的,比较可能是局部变量;1代和2代对象有可能是类变量或者全局变量,比较不可能是垃圾。0代对象在每次垃圾回收都会检查,1代对象大概是10次垃圾回收才检查一次,2代对象大概100次垃圾回收才检查一次。再次回到刚才的析构函数,如果采用析构函数,则该对象在第一次垃圾回收的时候并不能立刻回收,因此会成为1代对象,最起码要大概10次垃圾回收的时候才会再检查一次,如果这时候析构函数还没有执行完,则要100次垃圾回收后才会去检查。
当然,在C#中我们可以实现IDispose接口来实现这种需要自己回收资源的对象。
(1) 尽量使用变量初始化而不是赋值。
如果一个类的所有的构造函数都要将变量的值初始化到一个值,则可以在声明变量的时候进行初始化。这样可以保证该变量在所有的构造函数里面都被初始化。变量初始化是在所有构造函数执行前执行的。
如 public class A {priavate ArrayList _coll = new ArrayList(); }保证了_coll变量在所有的类实例中都被初始化了。
但有三种情况下是不能使用变量初始化的
(a) 如果变量初始值为0或者null。这些在变量声明的时候就已经自动初始化为0了,用户再初始化会降低效率。特别是对于值类型的变量。比如 MyValueType _myValue;这样声明就已经将这个变量的值初始化为0了。而如果这样声明:MyValueType _myValue = new MyValueType();这样是使用中间语言来初始化这个变量了,造成box和unbox的问题,效率是不高的。
(b) 如果一个变量在不同的构造函数被初始成的值是不同的,则要在每个构造函数里面进行单独的初始化,不然有些值会被初始化两次。如
public class MyClass
{
private ArrayList _coll = new ArrayList();
MyClass() {}
MyClass(int size) { _coll = new ArrayList( size ); }
}
则通过编译器生成的代码会类似如下(这当然是我们所不喜欢的):
public class MyClass
{
private ArrayList _coll;
MyClass() { _coll = new ArrayList(); }
MyClass(int size)
{
_coll = new ArrayList();
_coll = new ArrayList(size);
}
}
(c) 如果变量初始化的时候要处理异常的。因为声明变量的时候初始化并没有办法处理异常。
(2) 静态构造函数
静态构造函数在静态变量的初始化调用之后。静态构造函数可以初始化静态变量,实现Singleton。
(3) 使用构造函数的嵌套来重构构造函数的代码
如果多个构造函数存在公共的代码,应该采用构造函数的嵌套来实现重构,而不是通过公共的帮助函数来实现重构。虽然在代码的执行效率上是一样的(注:书上的说法有点错误,书上说代码的效率会提高),但生成的代码是比较干净的。比如:
public class MyClass
{
public MyClass() { ComonFunc("Default"); }
public MyClass(string val) { CommonFunc(val); }
}
当C#编译器生成相应的代码的时候,会生成大概如下的代码:
public class MyClass
{
public MyClass()
{
// 初始化变量
// 调用基类的构造函数
CommonFunc( "Default" );
}
public MyClass(string val)
{
// 初始化变量
// 调用基类的构造函数
CommonFunc( val );
}
}
而如果我们在MyClass()里面调用this("Default"),即调用另一个构造函数,则C#编译器只会在MyClass(string val)里面生成初始化变量和调用基类的构造函数的代码,而不会在MyClass()里面也生成重复的代码。
(4) Using 语句
如果对象实现了IDispose接口,要使用using语句来翻译未托管的内存。使用using语句,C#编译器会生成Try{}Finally{}语句来保证Dispose方法的调用。
将using语句使用在未实现IDispose接口的对象上,会产生编译错误。如果我们不能确定一个对象是否实现了IDispose接口,但又想保证其能够正确地释放所使用的资源,可以使用以下的方法:Object obj = ...; using (obj as IDispose) {...},如果obj实现了IDispose接口,则其资源能够正确释放;如果obj未实现IDispose接口,则生成using(null),这样不会出现任何的问题,除了该语句不作任何事情以外。
如果我们在语句中使用到了嵌套的using,则可以使用自己写的Try{}Finally{}语句,使得生成的代码更简单一些。比如 using (A a = new A()) { using (B b = new B()) {...} },会产生类似以下的语句
A a = null;
try {
a = new A();
B b = null;
try {
b = new B();
} finally {
if (b != null) b.Dispose();
}
} finally {
if (a != null) a.Dispose();
}
该语句生成嵌套的Try{}Finally{}语句,使得代码稍显复杂,可以自己重构成如下的Try{}Finally{}语句,代码会简单很多。
A a = null;
B b = null;
try {
a = new A();
b = new B();
} finally {
if (b != null) b.Dispose();
if (a != null) a.Dispose();
}
有些类可能既实现了Dispose方法,又实现了Close方法,比如SqlConnection就实现了这两种方法。如果我们写成如下的语句:
try {
sqlConnection = new SqlConnection();
} finally {
if (sqlConnection != null) sqlConnection.Close();
}
虽然SqlConnection的连接也能够被正确关闭,但是Close()方法和Dispose()所做的事情是不同的。Dispose做了更多的事情:Dispose方法通知GC这个对象已经不需要再调用“析构函数”了,因此GC在回收资源的时候,可以直接将这个对象所用的内在回收。但如果只用了Close方法,则GC在回收资源的时候仍然需要调用其“析构函数”,并暂时将其对象放入析构队列中。还记得这种方法的坏处吗?上面提过。因此要使用Dispose方法来释放资源,如果这个对象实现了IDispose接口的话。
(5) 减少内存垃圾
虽然GC的功能很强大,但是我们也要尽量在写代码的时候减少内存的垃圾
(a) 可以考虑把一些经常被调用的函数里面的引用类型的变量提升为类变量,如果这些引用类型的变量值是固定的话。比如如果在一个OnPaint函数里面创建Font myFont = new Font("Arial", 10.0f),因为OnPaint函数是Windows经常调用的函数,而myFont是一个固定的引用类型的变量,这种情况下就可以将myFont定义成类的变量。但要记住如果把实现了IDispose的变量提升为类变量,则需要为这个类实现IDispose接口以正确释放资源。
(b) 可以考虑为类提供一些常用的Singleton的实例,这样应用程序在使用到这些常用的实例的时候,就不需要重新创建一个将成为垃圾的实例了。比如Brush类就提供了很多常用的Singleton实例,比如Brush.Black.
(c) 为不可变的类型提供构建类。比如String就是一个不可变类,.NET提供了StringBuilder来构建String实例。
(6) 减少Boxing和UnBoxing的操作
在.NET中,因为所有的类的基类都是System.Object,但值类型不是多态的,这两者产生了矛盾。.NET采用了Boxing和UnBoxing(我不知如何翻译)来解决这个问题,当在需要System.Object的对象中遇到值类型时,.NET会在堆上分配一个内存,用来存储值类型对象,该内存是引用类型的,这样就叫Boxing;当程序需要对该内存上的对象进行访问时,会拷贝一个新的备份进行访问,这样就叫UnBoxing。Boxing和UnBoxing会产生性能上的问题,同时由于产生临时的拷贝对象,会产生一些微妙的Bug。
要尽量避免Boxing和UnBoxing的操作。如在以下的代码Console.WriteLine("{0}", 5);就会产生Boxing和UnBoxing的操作,因为5是一个值类型的对象,而Console.WriteLine需要的对象是System.Object。.NET编译器会产生类似以下的代码:
int i = 5;
object o = i;
Console.WriteLine (o.ToString());
在o对象上调用ToString会产生UnBoxing的操作:
object o;
int i = (int)o;
string output = i.ToString();
而如果我们写成这样Console.WriteLine("{0}", 5.ToString());则可以很好地避免Boxing和UnBoxing的操作。
另一个很容易产生这种Boxing和UnBoxing的操作的地方是使用.Net1.x里面的集合对象时,因为这些集合对象都是接受System.Object对象的,因此值类型会产生Boxing和UnBoxing的操作。而操作这些集合对象中的元素果,要特别留意其取得的值是原来值的一份拷贝。比如ArrayList list; list[0]取得的是值类型的一份拷贝,在其上的操作都不会作用到ArrayList存储的对象。我们可以采用继承接口的方法来使得从集合对象中取出的对象不是拷贝,而是实际的引用。比如:
public interface IPersonName { string Name {get; set;} }
struct Person: IPersonName { ... }
这样ArrayList list; 当存储Person对象时,((IPersonName)list[0]).Name = "New Value"实际上是会将值作用在ArrayList中的元素的。因为在Boxing操作时,其产生的引用类型对象会实现值类型的所有接口操作,因此对这些接口进行操作时是不需要UnBoxing操作的。
(6) 实现标准的Dispose模式
如果类里面使用了未托管的资源,则需要为该类实现IDisposable接口。该接口只有一个函数Dispose,在该函数里面释放未托管的资源,但同时我们也要实现析构函数,以防止用户使用的时候忘记调用Dispose方法。该Dispose函数应该完成以下的四个目的:
(a) 释放未托管的资源
(b) 翻译托管的资源,包括删除事件等
(c) 设置一个标记,说明该对象的Dispose函数已经被调用了。因为Dispose函数可能会被调用多次,但只有第一次调用的时候才需要真正释放资源
(d) 压制析构函数的调用,GC.SuppressFinalize(true)。这样GC在回收资源的时候就不会去调用其析构函数了,该对象占用的内在资源能够被很快地释放掉。
以下是Dispose方法常用的实现模式:
public class MyResourceHog : IDisposable
{
private bool _alreadyDisposed = false;
~MyResourceHog() { Dispose( false ); }
public void Dispose() { Dispose ( true ); GC.SuppressFinalize( true ); }
protected virtual void Dispose (bool isDisposing)
{
if (_alreadyDisposed) return;
if (isDisposing) { //释放托管的资源 }
//释放未托管的资源
_alreadyDisposed = true;
}
}
之所以使用一个Virtual的Dispose(bool isDisposing)函数是为了使继承的类能够更好地实现其Dispose和析构函数。如果继承的类需要释放其未托管的资源,只需要如下:
public class DerivedResourceHog : MyResourceHog
{
private bool _disposed = false;
protected virtual void Dispose (bool isDisposing)
{
if (_disposed) return;
if (isDisposing) { //释放托管的资源 }
//释放未托管的资源
base.Dispose(isDisposing);
_disposed = true;
}
}
之所以继承的类也同时定义一个标记来表示该类是否已经被释放了,其实是防御性的做法:Duplicating the flag encapsulates any possible mistakes made while disposing of an object to only the one type, not all types that make up an object(还没参透)。Dispose方法可能会被调用多次,并且不同对象的Dispose方法被调用的顺序是无法预知的。
在Dispose方法和析构函数中只能做释放资源的动作,而不应该再进行其它的逻辑。