Effective C# 原则16:垃圾最小化

JerryXia 发表于 , 阅读 (862)

垃圾回收器对内存管理表现的非常出色,并且它以非常高效的方法移除不再使用的对象。但不管你怎样看它,申请和释放一个基于堆内存的对象总比申请和释放一个不基于堆内存的对象要花上更多的处理器时间。你可以给出一些严重的性能问题,例如应用程序在某个方法内分配过量的引用对象。

你不应该让垃圾回收器超负荷的工作,为了程序的效率,你可以使用一些简单的技巧来减少垃圾回收器的工作。所有的引用类型,即使是局部变量,都是在堆上分配的。所有引用类型的局部变量在函数退出后马上成为垃圾,一个最常见的“垃圾”做法就是申请一个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接口。原则18会给你解释如何正确的完成它。

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

前面例子中使用的静态属性Brushes.Black,演示了另一个避免重复创建相似对象的技术。使用静态成员变量来创建一些常用的引用类型的实例。考虑前面那个例子里使用的黑色画刷,每次当你要用黑色画刷来画一些东西时,你要在程序中创建和释放大量的黑色画刷。前面的一个解决方案就是在每个期望黑色画刷的类中添加一个画刷成员,但这还不够。程序可能会创建大量的窗口和控件,这同样会创建大量的黑色画刷。.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)的画刷就可能永远不会创建。框架提供了一个方法来限制对象,使得在满足目标的情况下使用最小的对象集合。学会在你的应用程序里使用这样的技巧。

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

string msg = "Hello, ";
msg += thisUser.Name;
msg += ". Today is ";
msg += System.DateTime.Now.ToString();

这实际上低效的如果你是这样写:

string msg = "Hello, ";
// Not legal, for illustration only:
string tmp1 = new String( msg + thisUser.Name );
string msg = tmp1; // "Hello " is garbage.
string tmp2 = new String( msg + ". Today is " );
msg = tmp2; // "Hello <user>" is garbage.
string tmp3 = new String( msg + DateTime.Now.ToString( ) );
msg = tmp3;// "Hello <user>. Today is " is garbage.

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

(译注:请理解作者的意图,只有当你使用恒定类型时才这样,如果是引用类型,就不一定非要使用对象生成器了。而且注意恒定类型的特点,就是一但创建就永远不能改变,所有的修改都会产生新的实例,string就是一个典型的例子,它是一个恒定的引用类型;还有DateTime也是一个,它是一个恒定的值类型。)

垃圾回收器在管理应用程序的内存上确实很高效。但请记住,创建和释放堆对象还是很占时间的。避免创建大量的对象,也不要创建你不使用的对象。也要避免在局部函数上多次创建引用对象。相反,把局部变量提供为类型成员变量,或者把你最常用的对象实例创建为静态对象。最后,考虑使用可变对象创建器来构造恒定对象。

添加新评论