Effective C# 原则16:垃圾最小化
Item 16: Minimize Garbage
垃圾回收器对内存管理表现的非常出色,并且它以非常高效的方法移除不再使用的对象。但不管你怎样看它,申请和释放一个基于堆内存的对象总比申请和释放一个不基于堆内存的对象要花上更多的处理器时间。你可以给出一些严重的性能问题,例如应用程序在某个方法内分配过量的引用对象。
你不应该让垃圾回收器超负荷的工作,为了程序的效率,你可以使用一些简单的技巧来减少垃圾回收器的工作。所有的引用类型,即使是局部变量,都是在堆上分配的。所有引用类型的局部变量在函数退出后马上成为垃圾,一个最常见的“垃圾”做法就是申请一个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也是一个,它是一个恒定的值类型。)
垃圾回收器在管理应用程序的内存上确实很高效。但请记住,创建和释放堆对象还是很占时间的。避免创建大量的对象,也不要创建你不使用的对象。也要避免在局部函数上多次创建引用对象。相反,把局部变量提供为类型成员变量,或者把你最常用的对象实例创建为静态对象。最后,考虑使用可变对象创建器来构造恒定对象。
======================
Item 16: Minimize Garbage
The Garbage Collector does an excellent job of managing memory for you, and it removes unused objects in a very efficient manner.But no matter how you look at it, allocating and destroying a heap-based object takes more processor time than not allocating and not destroying a heap-based object. You can introduce serious performance drains on your program by creating an excessive number of reference objects that are local to your methods.
So don't overwork the Garbage Collector. You can follow some simple techniques to minimize the amount of work that the Garbage Collector needs to do on your program's behalf. All reference types, even local variables, are allocated on the heap. Every local variable of a reference type becomes garbage as soon as that function exits. One very common bad practice is to allocate GDI objects in a Windows paint handler:
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() gets called frequently. Every time it gets called, you create another Font object that contains the exact same settings. The Garbage Collector needs to clean those up for you every time. That's incredibly inefficient.
Instead, promote the Font object from a local variable to a member variable. Reuse the same font each time you paint the window:
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 );
}
Your program no longer creates garbage with every paint event. The Garbage Collector does less work. Your program runs just a little faster. When you elevate a local variable, such as a font, that implements IDisposable to a member variable, you need to implement IDisposable in your class. Item 18 explains how to properly do just that.
You should promote local variables to member variables when they are reference types (value types don't matter) and they will be used in routines that are called very frequently. The font in the paint routine makes an excellent example. Only local variables in routines that are frequently accessed are good candidates. Infrequently called routines are not. You're trying to avoid creating the same objects repeatedly, not turn every local variable into a member variable.
The static property Brushes.Black, used earlier illustrates another technique that you should use to avoid repeatedly allocating similar objects. Create static member variables for commonly used instances of the reference types you need. Consider the black brush used earlier as an example. Every time you need to draw something in your window using the color black, you need a black brush. If you allocate a new one every time you draw anything, you create and destroy a huge number of black brushes during the course of a program. The first approach of creating a black brush as a member of each of your types helps, but it doesn't go far enough. Programs might create dozens of windows and controls, and would create dozens of black brushes. The .NET Framework designers anticipated this and created a single black brush for you to reuse whenever you need it. The Brushes class contains a number of static Brush objects, each with a different common color. Internally, the Brushes class uses a lazy evaluation algorithm to create only those brushes you request. A simplified implementation looks like this:
private static Brush _blackBrush;
public static Brush Black
{
get
{
if ( _blackBrush == null )
_blackBrush = new SolidBrush( Color.Black );
return _blackBrush;
}
}
The first time you request a black brush, the Brushes class creates it. The Brushes class keeps a reference to the single black brush and returns that same handle whenever you request it again. The end result is that you create one black brush and reuse it forevermore. Furthermore, if your application does not need a particular resourcesay, the lime green brushit never gets created. The framework provides a way to limit the objects created to the minimum set you need to accomplish your goals. Copy that technique in your programs..
You've learned two techniques to minimize the number of allocations your program performs as it goes about its business. You can promote often-used local variables to member variables. You can provide a class that stores singleton objects that represent common instances of a given type. The last technique involves building the final value for immutable types. The System.String class is immutable: After you construct a string, the contents of that string cannot be modified. Whenever you write code that appears to modify the contents of a string, you are actually creating a new string object and leaving the old string object as garbage. This seemingly innocent practice:
string msg = "Hello, ";
msg += thisUser.Name;
msg += ". Today is ";
msg += System.DateTime.Now.ToString();
is just as inefficient as if you had written this:
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.
The strings tmp1, tmp2, and tmp3, and the originally constructed msg ("Hello"), are all garbage. The += method on the string class creates a new string object and returns that string. It does not modify the existing string by concatenating the characters to the original storage. For simple constructs such as the previous one, you should use the string. Format() method:
string msg = string.Format ( "Hello, {0}. Today is {1}",
thisUser.Name, DateTime.Now.ToString( ));
For more complicated string operations, you can use the StringBuilder class:
StringBuilder msg = new StringBuilder( "Hello, " );
msg.Append( thisUser.Name );
msg.Append( ". Today is " );
msg.Append( DateTime.Now.ToString());
string finalMsg = msg.ToString();
StringBuilder is the mutable string class used to build an immutable string object. It provides facilities for mutable strings that let you create and modify text data before you construct an immutable string object. Use StringBuilder to create the final version of a string object. More important, learn from that design idiom. When your designs call for immutable types (see Item 7), consider creating builder objects to facilitate the multiphase construction of the final object. That provides a way for users of your class to construct an object in steps, yet maintain the immutability of your type.
The Garbage Collector does an efficient job of managing the memory that your application uses. But remember that creating and destroying heap objects still takes time. Avoid creating excessive objects; don't create what you don't need. Also avoid creating multiple objects of reference types in local functions. Instead, consider promoting local variables to member variables, or create static objects of the most common instances of your types. Finally, consider creating mutable builder classes for immutable types.