您的位置:首页 > 编程语言

教你写出优雅的代码(四)

2010-04-07 20:13 225 查看
用自己有限的知识去尽量减少内存垃圾

我们学习的知识对于.NET这个庞大的家族来说,也许是很少的,但是,我们要用很少的知识,去做别人不能做的事情,这就是我经常感概,我上班的时候的第一个主管,为什么他的代码看上去总是那么平凡,却给我们带来了那么多回味.

基本上所有的.NET程序员都知道微软的垃圾回收器对内存的管理是非常出色的。并且用非常高效的方法去移出不再使用的对象。但是,不管怎么样,申请和释放一个基于堆内存的对象总比申请和释放一个不基于堆内存的对象要花上更多的处理器时间。值类型放在内存的栈里面,而引用类型是放在堆里面的,所以,值类型的释放要比引用类型的释放要节省时间。所以,如果在方法内创建了大量的引用,我们将会为程序引入严重的性能负担.

我们为了不让垃圾回收器超负荷的工作,也为了提供程序的运行效率,是可以用一些简单的技巧来减少垃圾回收器的工作。还有一点是我们应该必须明白的,就是,引用类型不管是局部的,还是不是局部的。所有引用类型都是在堆上分配的。所有的引用类型的局部变量在函数退出后马上就变成了"垃圾"。一个最常见的不良做法就是我们在做GDI+的时候,我们为了在一个windows绘图处理器中分配对象。

protected override void OnPaint( PaintEventArgs e )
{
// Bad. Created the same font every paint event.
using ( Font MyFont = new Font( "Arial", 10.0f ))
{
e.Graphics.DrawString( DateTime.Now.ToString(),MyFont, Brushes.Black, new PointF( 0,0 ));
}
base.OnPaint( e );
}
OnPaint()函数的调用很频繁的,每次调用它的时候,都会生成另一个Font对象,而实际上它是完全一样的内容。垃圾回收器每次都须要清理这些对象。这将是难以置信的低效。那么什么样的做法是优的呢?

取而代之的是,把Font对象从局部变量提供为对象成员,在每次绘制窗口时重用同样的对象:
private readonly Font _myFont = new Font( "Arial", 10.0f );
protected override void OnPaint( PaintEventArgs e )
{
e.Graphics.DrawString( DateTime.Now.ToString( ),_myFont, Brushes.Black, new PointF( 0,0 ));
base.OnPaint( e );
}

这样你的程序在每次paint事件发生时不会产生垃圾,垃圾回收器的工作减少了,你的程序运行会稍微快一点点。当你把一个实现了IDisposable接口的局部变量提升为类型成员时,例如字体,你的类同样也应该实现IDisposable接口。

结论1:当一个引用类型(值类型的就无所谓了)的局部变量在常规的函数调用中使用的非常频繁时,你应该把它提升为对象的成员。
那个字体就是一个很好的例子。只有常用的局部变量频繁访问时才是很好的候选对象,不是频繁调用的就不必了。你应该尽可能的避免重复的创建同样的对象,使用成员变量而不是局部变量。

回头来看,前面例子中使用的静态属性Brushes.Black,演示了另一个避免重复创建相似对象的技术。使用静态成员变量来创建一些常用的引用类型的实例。(我记得同学们经常问我为什么我们写的DBTool里面的方法全部都要使用静态的呢?所以你可以把这种做法也看成是一种解决问题的办法或者结论

考虑前面那个例子里使用的黑色画刷,每次当你要用黑色画刷来画一些东西时,你要在程序中创建和释放大量的黑色画刷。前面的一个解决方案就是在每个期望黑色画刷的类中添加一个画刷成员,但这还不够。程序可能会创建大量的窗口和控件,这同样会创建大量的黑色画刷。.Net框架的设计者预知了这个问题,他们为你创建一个简单的黑色画刷以便你在任何地方都可以重复使用。Brushes对象包含一定数量的静态Brush对象,每一个具有不同的常用的颜色。在内部,Brushes使用了惰性算法来,即只有当你使用时才创建这些对象(惰性算法:只有当你使用时才创建这些对象)。一个简单的实现方法:
private static Brush _blackBrush;
public static Brush Black
{
get
{
if ( _blackBrush == null )
_blackBrush = new SolidBrush( Color.Black );
return _blackBrush;
}
}

当你第一次申请黑色画刷时,Brushes类就会创建它。然而Brushes类就保留一个单一的黑色画刷的引用句柄,当你再次申请时它就直接返回这个句柄。结果就是你只创建了一个黑色画刷并且一直在重用它。另外,如果你的应用程序不须要一个特殊的资源,一个柠檬绿(lime green)的画刷就可能永远不会创建。框架提供了一个方法来限制对象,使得在满足目标的情况下使用最小的对象集合。(再三考虑下,还是希望大家多多看看C#基础以及模式以及内核的书,虽然,我已经好久不看了,但是,你还是必须要看的,本来想借几本ASP.NET的书给大家,后来,还是决定大家还是要再看看C#的书,以及模式,以及深度讲解.NET的书。)。

你已经学会了两种技术来最小化应用程序的(对象)分配数量[ 其实,最小化应用程序的(对象)的分配数量就是一种提高你程序效率的途径。],正如它承担它自己的任务一样。你可以把一个经常使用的局部变量提升为类的成员变量,你可以提供一个类以单件模式来存储一些常用的给定对象的实例。最后一项技术还包括创建恒定类型的最终使用值。System.String类就是一个恒定类型,在你创建一个字符串后,它的内容就不能更改了。当你编写代码来修改这些串的内容时,你实际上是创建了新的对象,并且让旧的串成为了垃圾。下面的代码看上去是清清白白:

string msg = "Hello, ";
msg += thisUser.Name;
msg += ". Today is ";
msg += System.DateTime.Now.ToString();
这实际上低效的,如果你是这样写:
string msg = "Hello, ";
//下面的代码也许不合法,仅仅只用于说明。
string tmp1 = new String( msg + thisUser.Name );
string msg = tmp1; // "Hello " 变成了垃圾
string tmp2 = new String( msg + ". Today is " );
msg = tmp2; // "Hello <user>" 变成了垃圾.
string tmp3 = new String( msg + DateTime.Now.ToString( ) );
msg = tmp3;// "Hello <user>. Today is " 变成了垃圾
字符串tmp1,tmp2,tmp3以及最原始的msg构造的(“Hello”),都成了垃圾。
+=方法在字符串类上会生成一个新的对象并返回它。
它不会通过把字符链接到原来的存储空间上来修改结果。

对于先前这个例子,给一个简单的构造例子,你应该使用string.Format()方法:
string msg = string.Format ( "Hello, {0}. Today is {1}",thisUser.Name, DateTime.Now.ToString( ));

对于更多的复杂的字符串操作,你应该使用StringBuilter类:
StringBuilder msg = new StringBuilder( "Hello, " );
msg.Append( thisUser.Name );
msg.Append( ". Today is " );
msg.Append( DateTime.Now.ToString());
string finalMsg = msg.ToString();

StringBuilder也一个(内容)可变的字符串类,用于生成恒定的字符串对象。在你还没有创建一个恒定的字符串对象前,它提供了一个有效的方法来存储可变的字符串。更重要的是,学习这样的设计习惯。对于一些要经过多次构造后才能最终得到的对象,可以考虑使用一些对象生成器来简化对象的创建。它提供了一个方法让你的用户来逐步的创建(你设计的)恒定类型,也用于维护这个类型。

垃圾回收器在管理应用程序的内存上确实很高效。但请记住,创建和释放堆对象还是要花费时间的。我们应该避免创建大量的对象,也不要创建你不使用的对象。也要避免在局部函数上多次创建引用对象。相反,把局部变量提供为类型成员变量,或者把你最常用的对象实例创建为静态对象。最后,对于具有常量性的类型,我们应该考虑为它们创建一个支持可变的生成器类。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: