using语句之-释放资源和异常处理 zz
using语句之-释放资源和异常处理
2009-05-23 14:04
有时,代码要求非托管资源,如文件句柄、COM 包装或 SQL 连接。在使用一个或多个此类资源完成了代码后,Using 块确保这些资源的释放。这样,其他代码就可以使用它们。
托管资源由 .NET Framework 垃圾回收器 (GC)释放,您不需要进行任何额外的编码。您也不需要用于托管资源的 Using 块。
Using 块有三个部分:获取、使用和释放。
- 获取表示创建变量并将其初始化,以便引用系统资源。Using 语句可获取一个或多个资源,您可以在进入块之前恰好获取一个资源,并将其提供给 Using 语句。如果提供 resourceexpression,在将控制权传递给 Using 语句之前,您必须获取资源。
- 使用表示访问资源并使用资源执行操作。Using 和 End Using 之间的语句代表资源的使用过程。
- 释放表示针对 resourcename 中的对象调用 Dispose 方法。这使该对象可以完全终止其非托管资源。End Using 语句释放 Using 块控制的资源。
行为Using 块的工作方式类似于 Try...Finally 构造,在该构造中,Try 块使用资源,而
Finally 块释放资源。因此,不管您如何退出块,Using 块都可确保资源的释放。即使发生未处理的异常(除 StackOverflowException 外),也是如此。
Using 语句获取的每个资源变量的范围仅限于 Using 块。
如果在 Using 语句中指定多个系统资源,效果就好像您将多个Using 块相互嵌套一样。
Using 块中的结构化异常处理如果需要处理可能发生在 Using 块中的异常,可以向该块中添加一个完整的 Try...Finally 构造。如果需要处理
Using 语句未成功获取资源的情况,可以进行测试,以确定 resourcename 是否为Nothing。
进行结构化异常处理而不使用 Using 块如果需要对资源的获取进行更细致的控制,或者需要 Finally 块中的附加代码,可以将
Using 块重写为 Try...Finally 构造。下面的示例显示主干
Try 和 Using 构造,这两个构造在获取和释放 resource 过程中是等效的。
在using语句内的资源必须重载IDispose这个接口,上面说的可以看出,using只负责执行所包含资源的一些操作,然后释放掉,并不是能够处理(using(){。。。。})大括号里的异常,如果担心内部发生异常,是要自己写try和catch的。。。using System;
class C :IDisposable
{
public voidUseLimitedResource()
{
Console.WriteLine("Usinglimited resource...");
}
voidIDisposable.Dispose()
{
Console.WriteLine("Disposinglimited resource.");
}
}
class Program
{
static voidMain()
{
using (C c =new C())
{
c.UseLimitedResource();
}
Console.WriteLine("Nowoutside using statement.");
Console.ReadLine();
}
}
系列二:资源管理(利用using,try/finally,减少内存垃圾)
1.我们常使用IDisposable接口的Dispose()方法来精确的释放非托管系统资源。在日常代码中我们确保调用Dispose()的方法是用using和try/catch/finally语句。
2.所有包含非托管资源的类型都会实现IDisposable接口,他们还会创建终结器,以防止我们忘记调用Dispose()。如果你忘记调用Dispose(),那些非内存资源会在晚些时候,终结器会调用时会释放这些资源。这就使得这些对象在内存时待的时间更长,从而会使你的应用程序会因系统资源占用太多而速度下降。
3.代码展示:
public void ExecuteCommand( string connString,stringcommandString )
{
SqlConnection myConnection = new SqlConnection( connString);
SqlCommand mySqlCommand = new SqlCommand(commandString,myConnection );
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
这个例子中的两个可处理对象没有被恰当的释放:SqlConnection和SqlCommand。两个对象同时保存在内存里直到析构函数被调用。(这两个类都是从System.ComponentModel.Component继承来的。) 解决这个问题的方法就是在使用完命令和链接后就调用它们的Dispose:
public void ExecuteCommand( string connString,stringcommandString )
{
SqlConnection myConnection = new SqlConnection(connString );
SqlCommand mySqlCommand = new SqlCommand(commandString,myConnection );
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
mySqlCommand.Dispose( );
myConnection.Dispose( );
}
但如果SQL命令在执行时抛出异常,这时你的Dispose()调用就永远不会成功。using语句可以确保Dispose()方法被调用。当你把对象分配到using语句内时,C#的编译器就把这些对象放到一个try/finally块内:
public void ExecuteCommand( string connString,stringcommandString )
{
using ( SqlConnection myConnection = new SqlConnection(connString ))
{
using ( SqlCommand mySqlCommand = new SqlCommand( commandString,myConnection ))
{
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
}
}
当你在一个函数内使用一个可处理对象时,using语句是最简单的方法来保证这个对象被恰当的处理掉。当这些对象被分配时,会被编译器放到一个try/finally块中。下面的两段代码编译成的IL是一样的:
SqlConnection myConnection = null;// Example Using clause:
using ( myConnection = new SqlConnection( connString ))
{
myConnection.Open();
}
// example Try / Catch block:
try {
myConnection = new SqlConnection( connString );
myConnection.Open();
}
finally {
myConnection.Dispose( );
}
4.我们不能对一个没有实现IDispose接口的对象使用using方式,编译器会报错。
5.每一个using语句生成了一个新的嵌套的try/finally块。我发现这是很糟糕的结构,所以,如果是遇到多个实现了IDisposable接口的对象时,我更愿意写自己的try/finally块:
public void ExecuteCommand( string connString,stringcommandString )
{
SqlConnection myConnection = null;
SqlCommand mySqlCommand = null;
try {
myConnection = new SqlConnection( connString );
mySqlCommand = new SqlCommand( commandString,myConnection );
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
finally
{
if (mySqlCommand != null )
mySqlCommand.Dispose();
if (myConnection != null )
myConnection.Dispose();
}
}
6.有一些对象同时支持Disponse和Close两个方法来释放资源。SqlConnection就是其中之一,你可以像这样关闭
public void ExecuteCommand( string connString,stringcommandString )
{
SqlConnection myConnection = null;
try {
myConnection = new SqlConnection( connString );
SqlCommand mySqlCommand = new SqlCommand( commandString,myConnection );
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
finally
{
if (myConnection != null )
myConnection.Close();
}
}
这个版本关闭了链接,但它却是与处理对象是不一样的。Dispose方法会释放更多的资源,它还会告诉GC,这个对象已经不再须要析构了(注:这个对象本身并没有删除出内存)。它是通过调用GC.SuppressFinalize()来实现这一点的。但Close()一般不会,因此调用了close()的对象仍然留在终结列表中,虽然这个时候不需要终结器的操作了。当你有选择时,Dispose()比Colse()要好。
7.我们应该对经常出现的局部变量提升为类的成员变量(当在一个方法中经常调用一个变量时候,方法调用结束后,垃圾收集器就会去处理这个变量,每调用一次垃圾收集器就要增加额外的开销),对那些是非托管资源的变量升级为成员变量我们还应该把类设计成继承IDisposable接口,在接口方法中释放非托管资源。下面举例说明:
protected override void OnPaint( PaintEventArgs e )
{
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 );
}
8.所有的引用类型(值类型没关系),即使是局部变量都分配在堆上。当垃圾回收器回收堆上的对象时候会花费而外的时间,随着函数的退出,引用类型的局部变量就会变成垃圾。这里强调的是在频繁的调用的时候,如果调用的很少我们没有必要把局部变量升级为成员变量。
9.前面例子中使用的静态属性Brushes.Black,演示了另一个避免重复创建相似对象的技术。使用静态成员变量来创建一些常用的引用类型的实例。考虑前面那个例子里使用的黑色画刷,每次当你要用黑色画刷来画一些东西时,你要在程序中创建和释放大量的黑色画刷。前面的一个解决方案就是在每个期望黑色画刷的类中添加一个画刷成员,但这还不够。程序可能会创建大量的窗口和控件,这同样会创建大量的黑色画刷。.Net框架的设计者预知了这个问题,他们为你创建一个简单的黑色画刷以便你在任何地方都可以重复使用。(也就是设计模式中的单例模式)
private static Brush _blackBrush;//类的静态成员
public static Brush Black//静态属性
{
get
{
if ( _blackBrush== null )//这里也可以用于多线程的处理
_blackBrush =new SolidBrush( Color.Black );
return _blackBrush;
}
}
10.System.String类就是一个恒定类型,在你创建一个字符串后,它的内容就不能更改了。当你编写代码来修改这些串的内容时,你实际上是创建了新的对象,并且让旧的串成为了垃圾。
string msg = "Hello, ";
msg += thisUser.Name;
msg += ". Today is ";
msg += System.DateTime.Now.ToString();
字符串tmp1,tmp2,tmp3以及最原始的msg构造的(“Hello”),都成了垃圾。+=方法在字符串类上会生成一个新的对象并返回它。对于更多的复杂的字符串操作,你应该使用StringBuilter类。通过StringBuilter学到了一种思想:一些要经过多次构造后才能最终得到的对象,可以考虑使用一些对象生成器来简化对象的创建。它提供了一个方法让你的用户来逐步的创建(你设计的)恒定类型,也用于维护这个类型。
11.初始化常量类型通常有三种策略,选择哪一种策略依赖于一个类型的复杂度。定义一组合适的构造器通常是最简单的策略。也可以创建一个工厂方法(factory method)来进行初始化工作。这种方式对于创建一些常用的值比较方便。.NET框架中的Color类型就采用了这种策略来初始化系统颜色。例如,静态 方法Color.FromKnownColor()和Color.FromName()可以根据一个指定的系统颜色名,来返回一个对应的颜色值。最后,对于需要多个步骤操作才能完整构造出一个常量类型的情况,我们可以通过创建一个可变的辅助类来解决。.NET中的String类就采用了这种策略,其辅助类为System.Text.StringBuilder。我们可以使用StringBuilder类通过多步操作来创建一个String对象。
1.垃圾收集器(GC)控制着托管内存。但是我们需要对非托管代码进行处理(数据库连接,文件句柄,GDI+对象,COM对象等)
2.垃圾收集器是运行在一个单独的线程中来移除程序中不再使用的内存。并且还会压缩托管堆,使得空闲内存能集中在一个连续的区域。
3.对于非托管资源我们需要在类型中定义一个终结器,以此来确保释放这些系统资源。在对象的内存被回收之前,系统会调用终结器,这样我们可以在终结器中编写我们的代码来释放该对象占有的非托管资源。值得注意的是对象的终结器是在对象成为垃圾之后调用的,但是在内存归还之前,但是我们无法精确的控制“不再使用对象”和“执行终结器”这2个事件之间的关系。
4.当GC发现某个对象是垃圾但是需要终结器时,它还不能直接从内存上删除这个对象。首先,它要调用终结器,但终结器的调用不是在垃圾回收器的同一个线程上运行的。取而代之的是,GC不得不把对象放置到终结器队列中,让另一个线程让执行所有的终结器。GC继续它自己的工作,从内存上移除其它的垃圾。在下一个GC回收时,那些被终结器了的对象才会再从内存上移除。
5.Net回收器定义了一种称为“代龄”的机制来优化。代可以帮助GC来很快的 标识那些看上去看是垃圾的对象。所有从上一次GC回收后开始创建的对象称为第0代对象,所有那些经过一次GC回收后还存在的对象称为第1代对象。所有那些 经过2次或者2次以上GC回收后还存在的对象称为第2代对象。代龄的目的就是用来区分临时变量以及一些应用程序的全局变量。第0代对象绝大部分是临时的变 量。成员变量,以及一些全局变量很快会成为第1代对象,最终成为第2代对象。GC通过限制检测第1以及第2代对象的频度来优化它的工作。每一次GC循环都 检测第0代对象,大概10次GC周期才会检查第0和第一代对象,大概100次GC周期才检查一次第0,1,2代对象。让我们考虑一下:一个须要终结器的对 象可能要比一个不用终结器的对象在内存里多待上9个GC回收循环。如果它还没有终结器,它将会移到第2代对象。在第2代对象中,一个可以生存上100个 GC循环直到下一个第2代集合。
6.记得一个垃圾回收器负责内存管理的托管环境的最大好处:内存泄漏,其它指针的相关的问题将不再存在。非内存资源迫使你要使用终结器来确保清理非内存资源。终结器会对你的应用程序性能产生一些影响,但你必须使用它们来防止资源泄漏。通过实现IDisposable接口并且使用它可以避免终结器在垃圾回收器上造成的性能损失。
Using Objects That Encapsulate Resources
When you write code that uses an object that encapsulates a resource, youshould make sure that the object's Dispose method gets called when youare finished using the object. You can do this with the C# usingstatement or by implementing a try/finally block in other languages that targetthe common language runtime.
C# Using Statement
The C# programming language's using statement makes a call to the Disposemethod more automatic, by simplifying the code that you must write to createand clean up an object. The using statement obtains one or moreresources, executes the statements that you specify, and then disposes of theobject. Note that the using statement is only useful for objects with alifetime that does not extend beyond the method in which the objects areconstructed. The following code example creates and cleans up an instance ofthe ResourceWrapper class, as illustrated in the C# example of implementing aDispose method.
class myApp
{
public static void Main()
{
using (ResourceWrapper r1 = new ResourceWrapper())
{
// Do something with the object.
r1.DoSomething();
}
}
}
The preceding code, incorporating the using statement, isequivalent to the following.
class myApp
{
public static void Main()
{
ResourceWrapper r1 = new ResourceWrapper();
try
{
// Do something with the object.
r1.DoSomething();
}
finally
{
// Check for a null resource.
if (r1 != null)
// Call the object's Dispose method.
r1.Dispose();
}
}
}
The C# using statement allows you to acquire multiple resources ina single statement, which is equivalent internally to nested usingstatements. For more information and a code example, see using Statement(C# Reference).
Try/Finally Block
When you write managed code that uses an object that encapsulates aresource in languages other than C#, use a try/finally block to ensure that acall is made to the object's Dispose method. The following code examplecreates and cleans up an instance of the Resource class, as illustratedin the Visual Basic example of implementing aDispose method.
class myApp
Public Shared Sub Main()
Resource r1 = new Resource()
Try
' Do something with the object.
r1.DoSomething()
Finally
' Check for a null resource.
If Not (r1 is Nothing) Then
' Call the object's Dispose method.
r1.Dispose()
End If
End Try
EndSub
End Class
1.垃圾收集器(GC)控制着托管内存。但是我们需要对非托管代码进行处理(数据库连接,文件句柄,GDI+对象,COM对象等)
2.垃圾收集器是运行在一个单独的线程中来移除程序中不再使用的内存。并且还会压缩托管堆,使得空闲内存能集中在一个连续的区域。
3.对于非托管资源我们需要在类型中定义一个终结器,以此来确保释放这些系统资源。在对象的内存被回收之前,系统会调用终结器,这样我们可以在终结器中编写我们的代码来释放该对象占有的非托管资源。值得注意的是对象的终结器是在对象成为垃圾之后调用的,但是在内存归还之前,但是我们无法精确的控制“不再使用对象”和“执行终结器”这2个事件之间的关系。
4.当GC发现某个对象是垃圾但是需要终结器时,它还不能直接从内存上删除这个对象。首先,它要调用终结器,但终结器的调用不是在垃圾回收器的同一个线程上运行的。取而代之的是,GC不得不把对象放置到终结器队列中,让另一个线程让执行所有的终结器。GC继续它自己的工作,从内存上移除其它的垃圾。在下一个GC回收时,那些被终结器了的对象才会再从内存上移除。
5.Net回收器定义了一种称为“代龄”的机制来优化。代可以帮助GC来很快的 标识那些看上去看是垃圾的对象。所有从上一次GC回收后开始创建的对象称为第0代对象,所有那些经过一次GC回收后还存在的对象称为第1代对象。所有那些 经过2次或者2次以上GC回收后还存在的对象称为第2代对象。代龄的目的就是用来区分临时变量以及一些应用程序的全局变量。第0代对象绝大部分是临时的变 量。成员变量,以及一些全局变量很快会成为第1代对象,最终成为第2代对象。GC通过限制检测第1以及第2代对象的频度来优化它的工作。每一次GC循环都 检测第0代对象,大概10次GC周期才会检查第0和第一代对象,大概100次GC周期才检查一次第0,1,2代对象。让我们考虑一下:一个须要终结器的对 象可能要比一个不用终结器的对象在内存里多待上9个GC回收循环。如果它还没有终结器,它将会移到第2代对象。在第2代对象中,一个可以生存上100个 GC循环直到下一个第2代集合。
6.记得一个垃圾回收器负责内存管理的托管环境的最大好处:内存泄漏,其它指针的相关的问题将不再存在。非内存资源迫使你要使用终结器来确保清理非内存资源。终结器会对你的应用程序性能产生一些影响,但你必须使用它们来防止资源泄漏。通过实现IDisposable接口并且使用它可以避免终结器在垃圾回收器上造成的性能损失。
1.初始化器(在声明字段的时候就直接初始化,而不是在构造函数中处理)
初始化生成的代码会放置在类型的构造函数之前。初始化会在执行类型的基类的构造函数之前被执行,并且它们是按你声明的先后关系顺序执行的。
2.什么情况下不应该初始化变量
首先就是,如果你是初始化一个对象为0,或者为null。系统默认会在你任何代码执行前,为所有的内容都初始化为0。系统置0的初始化是基于底层的CPU指令,对整个内存块设置。你的任何其它置0的初始化语句是多余的。C#编译器忠实的添加额外的指令把内存设置为0(多余的设置)。这并没有错,只是效率不高(牵涉装箱和拆箱)。事实上,如果是处理值类型数据,这是很不值的。
第二个低效率的是在你为一个对象添加两个构造函数时会产生。你使用初始化器初始化变量,而所有的构造函数也对这些变量进行了初始化。
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 );
}
}
当你创建一个新的MyClass对象时,特别指定集合的大小,实际上你创建了两个ArrayList数组列表。其中一个很快成为垃圾对象。初始化器在所有的构造函数之前会执行,这样它创建了一个ArrayList,之后构造函数会创建第2个ArrayList数组列表。
3.你应该知道,在一个类型的任何实例初始化以前,你应该初始化它的静态成员变量。在里C#你可以使用静态的初始化器和静态构造函数来实现这个目 的。一个类的静态构造函数是一个与众不同的,它在所有的方法,变量或者属性访问前被执行。你可以用这个函数来初始化静态成员变量,强制使用单件模式,或者实现其它任何在类型的实例可用前应该完成的工作。
同样,和实例的初始化器一样,静态的初始化器在静态的构造函数调用前执行。并且,你的静态初始化器在基类的静态构造函数执行前被执行。
4.构造第一个实例对象的操作顺序
1、将所有的静态变量设置我0。
2、执行静态变量初始化器。
3、执行基类的静态构造函数。
4、执行当前类型的静态构造函数。
5、将所有的实例变量设置为0。
6、执行实例变量初始化器。
7、执行适当的基类实例构造函数。
8、执行当前类型的实例构造函数。
后续的同样类型的实例从第5步开始,因为类的初始化器只执行一次。另外,第6和第7步是可以被优化放置在构造器初始化器中,这样编译器便可以移除重复的指令。
系列二:资源管理(利用using,try/finally,减少内存垃圾)
1.我们常使用IDisposable接口的Dispose()方法来精确的释放非托管系统资源。在日常代码中我们确保调用Dispose()的方法是用using和try/catch/finally语句。
2.所有包含非托管资源的类型都会实现IDisposable接口,他们还会创建终结器,以防止我们忘记调用Dispose()。如果你忘记调用Dispose(),那些非内存资源会在晚些时候,终结器会调用时会释放这些资源。这就使得这些对象在内存时待的时间更长,从而会使你的应用程序会因系统资源占用太多而速度下降。
3.代码展示:
public void ExecuteCommand( string connString,stringcommandString )
{
SqlConnection myConnection = new SqlConnection( connString);
SqlCommand mySqlCommand = new SqlCommand(commandString,myConnection );
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
这个例子中的两个可处理对象没有被恰当的释放:SqlConnection和SqlCommand。两个对象同时保存在内存里直到析构函数被调用。(这两个类都是从System.ComponentModel.Component继承来的。) 解决这个问题的方法就是在使用完命令和链接后就调用它们的Dispose:
public void ExecuteCommand( string connString,stringcommandString )
{
SqlConnection myConnection = new SqlConnection(connString );
SqlCommand mySqlCommand = new SqlCommand(commandString,myConnection );
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
mySqlCommand.Dispose( );
myConnection.Dispose( );
}
但如果SQL命令在执行时抛出异常,这时你的Dispose()调用就永远不会成功。using语句可以确保Dispose()方法被调用。当你把对象分配到using语句内时,C#的编译器就把这些对象放到一个try/finally块内:
public void ExecuteCommand( string connString,stringcommandString )
{
using ( SqlConnection myConnection = new SqlConnection(connString ))
{
using ( SqlCommand mySqlCommand = new SqlCommand( commandString,myConnection ))
{
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
}
}
当你在一个函数内使用一个可处理对象时,using语句是最简单的方法来保证这个对象被恰当的处理掉。当这些对象被分配时,会被编译器放到一个try/finally块中。下面的两段代码编译成的IL是一样的:
SqlConnection myConnection = null;// Example Using clause:
using ( myConnection = new SqlConnection( connString ))
{
myConnection.Open();
}
// example Try / Catch block:
try {
myConnection = new SqlConnection( connString );
myConnection.Open();
}
finally {
myConnection.Dispose( );
}
4.我们不能对一个没有实现IDispose接口的对象使用using方式,编译器会报错。
5.每一个using语句生成了一个新的嵌套的try/finally块。我发现这是很糟糕的结构,所以,如果是遇到多个实现了IDisposable接口的对象时,我更愿意写自己的try/finally块:
public void ExecuteCommand( string connString,stringcommandString )
{
SqlConnection myConnection = null;
SqlCommand mySqlCommand = null;
try {
myConnection = new SqlConnection( connString );
mySqlCommand = new SqlCommand( commandString,myConnection );
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
finally
{
if (mySqlCommand != null )
mySqlCommand.Dispose();
if (myConnection != null )
myConnection.Dispose();
}
}
6.有一些对象同时支持Disponse和Close两个方法来释放资源。SqlConnection就是其中之一,你可以像这样关闭
public void ExecuteCommand( string connString,stringcommandString )
{
SqlConnection myConnection = null;
try {
myConnection = new SqlConnection( connString );
SqlCommand mySqlCommand = new SqlCommand( commandString,myConnection );
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
finally
{
if (myConnection != null )
myConnection.Close();
}
}
这个版本关闭了链接,但它却是与处理对象是不一样的。Dispose方法会释放更多的资源,它还会告诉GC,这个对象已经不再须要析构了(注:这个对象本身并没有删除出内存)。它是通过调用GC.SuppressFinalize()来实现这一点的。但Close()一般不会,因此调用了close()的对象仍然留在终结列表中,虽然这个时候不需要终结器的操作了。当你有选择时,Dispose()比Colse()要好。
7.我们应该对经常出现的局部变量提升为类的成员变量(当在一个方法中经常调用一个变量时候,方法调用结束后,垃圾收集器就会去处理这个变量,每调用一次垃圾收集器就要增加额外的开销),对那些是非托管资源的变量升级为成员变量我们还应该把类设计成继承IDisposable接口,在接口方法中释放非托管资源。下面举例说明:
protected override void OnPaint( PaintEventArgs e )
{
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 );
}
8.所有的引用类型(值类型没关系),即使是局部变量都分配在堆上。当垃圾回收器回收堆上的对象时候会花费而外的时间,随着函数的退出,引用类型的局部变量就会变成垃圾。这里强调的是在频繁的调用的时候,如果调用的很少我们没有必要把局部变量升级为成员变量。
9.前面例子中使用的静态属性Brushes.Black,演示了另一个避免重复创建相似对象的技术。使用静态成员变量来创建一些常用的引用类型的实例。考虑前面那个例子里使用的黑色画刷,每次当你要用黑色画刷来画一些东西时,你要在程序中创建和释放大量的黑色画刷。前面的一个解决方案就是在每个期望黑色画刷的类中添加一个画刷成员,但这还不够。程序可能会创建大量的窗口和控件,这同样会创建大量的黑色画刷。.Net框架的设计者预知了这个问题,他们为你创建一个简单的黑色画刷以便你在任何地方都可以重复使用。(也就是设计模式中的单例模式)
private static Brush _blackBrush;//类的静态成员
public static Brush Black//静态属性
{
get
{
if ( _blackBrush== null )//这里也可以用于多线程的处理
_blackBrush =new SolidBrush( Color.Black );
return _blackBrush;
}
}
10.System.String类就是一个恒定类型,在你创建一个字符串后,它的内容就不能更改了。当你编写代码来修改这些串的内容时,你实际上是创建了新的对象,并且让旧的串成为了垃圾。
string msg = "Hello, ";
msg += thisUser.Name;
msg += ". Today is ";
msg += System.DateTime.Now.ToString();
字符串tmp1,tmp2,tmp3以及最原始的msg构造的(“Hello”),都成了垃圾。+=方法在字符串类上会生成一个新的对象并返回它。对于更多的复杂的字符串操作,你应该使用StringBuilter类。通过StringBuilter学到了一种思想:一些要经过多次构造后才能最终得到的对象,可以考虑使用一些对象生成器来简化对象的创建。它提供了一个方法让你的用户来逐步的创建(你设计的)恒定类型,也用于维护这个类型。
11.初始化常量类型通常有三种策略,选择哪一种策略依赖于一个类型的复杂度。定义一组合适的构造器通常是最简单的策略。也可以创建一个工厂方法(factory method)来进行初始化工作。这种方式对于创建一些常用的值比较方便。.NET框架中的Color类型就采用了这种策略来初始化系统颜色。例如,静态 方法Color.FromKnownColor()和Color.FromName()可以根据一个指定的系统颜色名,来返回一个对应的颜色值。最后,对于需要多个步骤操作才能完整构造出一个常量类型的情况,我们可以通过创建一个可变的辅助类来解决。.NET中的String类就采用了这种策略,其辅助类为System.Text.StringBuilder。我们可以使用StringBuilder类通过多步操作来创建一个String对象。
系列二:资源管理(减少装箱与拆箱,实现标准dispose)
1.值类型是数据的容器,它们不具备多态性。另一方面就是说,.Net框架被设计成单一继承的引用类型,System.Object,在整个继承关系中作为根对象存在。设计这两种类型的目的是截然不同的,.Net框架使用了装箱与拆箱来链接两种不同类型的数据。
2.装箱是把一个值类型数据放置在一个无类型的引用对象上,从而使一个值类型在须要时可以当成引用类型来使用。拆箱则是额外的从“箱”上拷贝一份值类型数据。装箱和拆箱可以让你在须要使用System.Object对象的地方使用值类型数据。但装箱与拆箱操作却是性能的强盗,在些时候装箱与拆箱会产生一些临时对象,它会导致程序存在一些隐藏的BUG。应该尽可能的避免使用装箱与拆箱。
3.装箱可以把一个值类型数据转化也一个引用类型,一个新的引用对象在堆上创建,它就是这个“箱子”,值类型的数据就在这个引用类型中存储了一份拷贝。参见图2.3,演示了装箱的对象是如何访问和存储的。箱子中包含一份这个值类型对象的拷贝,并且复制实现了已经装箱对象的接口。当你想从这个箱子中取回任何内容时,一个值类型数据的拷贝会被创建并返回。这就是装箱与拆箱的关键性概念:对象的一个拷贝存放到箱子中,而不管何时你再访问这个箱子时,另一个拷贝又会被创建。
4.Console.WriteLine("A few numbers:{0}, {1},{2}", 25, 32, 50);
使用重载的Console.WriteLine函数须要一个System.Object类型的数组引用,整型是值类型,所以必须装箱后才能传给重载的WriteLine方法。唯一可以强制这三个整数成为System.Object对象的方法就是把它们装箱。另外,在WriteLine内部,通过调用箱子对象上的ToString()方法来到达箱子内部。某种意义上讲,你生成了这样的结构:
int i =25;
object o = i; // box
Console.WriteLine(o.ToString());
在WriteLine内部,下面的执行了下面的代码:
object o;
int i = ( int )o; // unbox
string output = i.ToString( );
你可能自己从来不会写这样的代码,但是,却让编译器自动从一个指定的类型转化为System.Object,这确实是你做的。
为了避免这么挑剔的惩罚,在使用它们来调用WriteLine之前,你自己应该把你的类型转化成字符串的实例。 console.WriteLine("A few numbers:{0}, {1},{2}",25.ToString(), 32.ToString(), 50.ToString());
这段代码使用已知的整数类型,而且值类型再也不会隐式的转化为System.Object类型。这个常见的例子展示了避免装箱的第一个规则:注意隐式的转化为System.Object,如果可以避免,值类型不应该被System.Object代替。
5.
public struct Person
{
private string _Name;
public string Name
{
get
{ return_Name; }
set
{ _Name =value; }
}
public override string ToString( )
{ Return _Name; }
}
ArrayList attendees = new ArrayList( );
Person p = new Person( "Old Name" );
attendees.Add( p );
Person p2 = (( Person )attendees[ 0 ] );
p2.Name = "New Name";
Console.WriteLine(attendees[ 0 ].ToString( ));
Person是一个值类型数据,在存储到ArrayList之前它被装箱。这会产生一个拷贝。而在移出的Persone对象上通过访问属性做一些修改时,另一个拷贝被创建。而你所做的修改只是针对的拷贝,而实际上还有第三个拷贝通过ToString()方法来访问attendees[0]中的对象。
正因为这以及其它一些原因,你应该创建一些恒定的值类型(参见条款7)。如果你非要在集合中使用可变的值类型,那就使用System.Array类,它是类型安全的。
如果一个数组不是一个合理的集合,以C#1.x中你可以通过使用接口来修正这个错误。尽量选择一些接口而不是公共的方法,来访问箱子的内部去修改数据:
public interface IPersonName
{
string Name
{ get; set; }
}
struct Person : IPersonName
{
private string _Name;
public string Name
{
get { return_Name; }
set { _Name= value; }
}
public override string ToString( )
{
return _Name;
}
}
ArrayList attendees = new ArrayList( );
Person p = new Person( "Old Name" );
attendees.Add( p ); // 装箱
(( IPersonName )attendees[0 ] ).Name = "New Name";//这里用的是接口的方法,接口是引用类型,不需要拆箱
Console.WriteLine(attendees[ 0 ].ToString( )); // 拆箱
装箱后的引用类型会实现原数据类型上所有已经实现的接口。这就是说,不用做拷贝,你可以通过调用箱子上的IPersonaName.Name方法来直接访问请求到箱子内部的值类型数据。在值类型上创建的接口可以让你访问集合里的箱子的内部,从而直接修改它的值。在值类型上实现的接口并没有让值类型成为多态的,这又会引入装箱的惩罚
我们应该对任何将值类型转化成System.Object或者接口类型的构造保持密切的关注:把值类型放到集合里,调用定义参数为System.Object类型的方法,或者强制转化为System.Object。只要有可能,我们都应该避免这些构造!
6.讨论如何写代码来管理这些类占用的非内存资源了。一个标准的模式就是利用.Net框架提供的方法处理非内存资源。你的用户也希望你遵守这个标准 的模式。也就是通过实现IDisposable接口来释放非托管的资源,当然是在用户记得调用它的时候,但如果用户忘记了,析构函数也会被动的执行。它是和垃圾回收器一起工作的,确保在一些必要时候,你的对象只会受到因析构函数而造成的性能损失。这正是管理非托管资源的好方法,因此有必要彻底的弄明白它。
7.处在类继承关系中顶层的基类应该实现IDisposable接口来释放资源。这个类型也应该添加一个析构函数,做为最后的被动机制。这两个方法都应该是用虚方法来释放资源,这样可以让它的派生类重载这个函数来释放它们自己的资源。派生类只有在它自己须要释放资源时才重载这个函数,并且一定要记得调用基类的虚方法。
8. 开始时,如果你的类使用了非内存资源,则一定得有一个析构函数。你不能指望你的用户总是记得调用Dispose方法,否则当他们忘记时,你会丢失一些资源
9. 当垃圾回收器运行时,它会直接从内存中移除不用析构的垃圾对象。而其它有析构函数的对象还保留在内存中。这些对象被添加到一个析构队列中,垃圾回收器会起动一个线程专门来析构这些对象。当析构线程完成它的工作后,这些垃圾对象就可以从内存中移除了。就是说,须要析构的对象比不须要析构的对象 在内存中待的时间要长。但你没得选择。如果你是采用的这种被动模式,当你的类型占用非托管资源时,你就必须写一个析构函数。但目前你还不用担心性能问题,下一步就保证你的用户使用更加简单,而且可以避免因为析构函数而造成的性能损失。实现IDisposable接口是一个标准的模式来告诉用户和进行时系统:你的对象占有资源而且必须及时的释放。IDisposable接口只有一个方法
public interface IDisposable
{
void Dispose( );
}
实现IDisposable.Dispose()方法有责任完成下面的任务:
1、释放所有的非托管资源。
2、释放所有的托管资源(包括卸载一些事件)。
3、设置一个安全的标记来标识对象已经调用dispose()方法。如果在已经处理过的对象上调用任何方法时,你可以检验这个标记并且抛出一个ObjectDisposed的异常。
4、阻止析构。你要调用GC.SuppressFinalize(this)来完成最后的工作。
通过实现IDisposable接口,你写成了两件事:第一就是提供了一个机制来及时的释放所有占用的托管资源,另一个就是你提供了一个标准的模式让用户来释放非托管资源。这是十分重要的,当你在你的类型上实现了IDisposable接口以后,用户就可以避免析构时的损失。你的类就成了.Net社 区中表现相当良好的成员。
10.但在你创建的机制中还是存在一些漏洞。如何让一个派生类清理自己的资源,同时还可以让基类很好的再做资源清理呢?(译注:因为调用 Dispose方法时,必须调用基类的Dispose,当然是在基类有这个方法时。但前面说过,我们只有一个标记来标识对象是否处理过,不管先调用那个, 总得有一个方法不能处理这个标记,而这就存在隐患) 如果派生类重载了析构函数,或者自己添加实现了IDisposable接口,而这些方法又都是必须调用基类的方法的;否则,基类无法恰当的释放资源。同样,析构和处理共享了一些相同的职责:几乎可以肯定你是复制了析构方法和处理方法之间的代码。正如你会在原则26中学到的,重载接口的方法根本没有如你所期望的那样工作。Dispose标准模式中的第三个方法,通过一个受保护的辅助性虚函数,制造出它们的常规任务并且挂接到派生类来释放资源。基类包含接口的核心代码, 派生类提供的Dispose()虚函数或者析构函数来负责清理资源:
protected virtual void Dispose( bool isDisposing );
重载的方法Dispose(bool)同时完成析构和处理必须提供的任务,又因为它是虚函数,它为所有的派生类提供函数入口点。派生类可以重载这个函数,提供恰当的实现来释放它自己的资源,并且调用基类的函数释放基类资源。当isDisposing为true时你可能同时清理托管资源和非托管资源, 当isDisposing为false时你只能清理非托管资源。两种情况下,都可以调用基类的Dispose(bool)方法让它去清理它自己的资源。
当你实现这样的模式时,这里有一个简单的例子。MyResourceHog 类展示了IDisposable的实现,一个析构函数,并且创建了一个虚的Dispose方法:
public class MyResourceHog : IDisposable
{
// 标识是否调用过dispose()
private bool _alreadyDisposed = false;
// 终结器,调用虚方法
~MyResourceHog()
{
Dispose( false );
}
// dispose方法,调用虚方法,终止析构函数
public void Dispose()
{
Dispose( true );
GC.SuppressFinalize( true );
}
// Dispose虚方法
protected virtual void Dispose( bool isDisposing )
{
// 不释放多次
if ( _alreadyDisposed )
return;
if ( isDisposing )
{
// TODO: 释放托管资源.
}
// TODO: 释放非托管资源.
// 设置是否已经执行dispose:
_alreadyDisposed = true;
}
}
如果派生类有另外的清理任务,就让它实现Dispose方法:
public class DerivedResourceHog : MyResourceHog
{
// 设置自己的标识符.
private bool _disposed = false;
protected override void Dispose( bool isDisposing )
{
// 不能多次调用.
if ( _disposed )
return;
if ( isDisposing )
{
// TODO: 释放自己的托管资源.
}
// TODO: 释放非托管资源.
// 让基类释放自己的资源.
// 基类会调用 GC.SuppressFinalize( )处理析构函数
base.Dispose( isDisposing );
// 设置调用标识:
_disposed = true;
}
}
注意,派生类和基类都有一个处理状态的标记,这完全是防御性的。重制的标记掩盖了在处理时任何可能发生的错误,而且是单一的类型处理,而不是处理构 成这个对象的所有类型。你应该防御性的写处理方法dispose和析构函数,处理对象可能以任何顺序发生,你可能会遇到这种情况:你的类中某个成员在你调 用Dispose方法以前已经被释放过了。你没有看到这种情况是因为Dispose()方法是可以多次调用的。如果在一个已经被处理过的对象上调用该方法,就什么也不发生。析构函数也有同样的规则。任何对象的引用存在于内存中时,你不用检测null引用。然而,你引用的对象可能已经处理掉了,或者它已经析构了。
这就引入用了一个非常重要的忠告:对于任何与处理和资源清理相关的方法,你必须只释放资源! 不要在处理过程中添加其它任何的任务。你在处理和清理中添加其它任务时,可能会在对象的生存期中遇到一些严重而繁杂的问题。对象在你创建它时出生,在垃圾回收器认领它时死亡。你可以认为当你的程 序不能再访问它们时,它们是睡眠的。你无法访问对象,无法调用对象的方法。种种迹象表明,它们就像是死的。但对象在宣布死亡前,析构函数还有最后一气。析构函数什么也不应该做,就是清理非托管资源。如果析构函数通过某些方法让对象又变得可访问,那么它就复活了。(译注:析构函数不是用户调用的,也不由.Net系统调用,而是在由GC产生的额外线程上运行的) 它又活了,但这并不好。即使是它是从睡眼中唤醒的。这里有一个明显的例子:
public class BadClass
{
// 存储全局对象引用:
private readonly ArrayList _finalizedList;
private string _msg;
public BadClass( ArrayList badList, string msg )
{
// 对引用进行缓存
_finalizedList = badList;
_msg = (string)msg.Clone();
}
~BadClass()
{
// 将对象加入 _finalizedList.
// 对象又复活了,而不是垃圾
_finalizedList.Add( this );
}
}
当一个BadClass对象的析构函数执行时,它把自己的一个引用添加到了全局的链表中。这使得它自己又是可达的,它就又活了。前面向你介绍的这个方法会遇到一些让人畏缩的难题。对象已经被析构了,所以垃圾回收器从此相信再也不用调用它的析构函数了。如果你实际要析构一个可达对象,这将不会成功。其次,你 的一些资源可能不再有用。GC不再从内存上移除那些只被析构队列引用的对象,但它们可能已经析构了。如果是这样,它们很可能已经不能使用了。(译注:也就 是说利用上面的那个方法让对象复活后,很有可能对象是不可用的。)尽管BadClass所拥有的成员还在内存里,它们像是可以被析构或者处理,但C#语言没有一个方法可以让你控制析构的次序,你不能让这样的结构可靠的运行。不要尝试。
我还没有看到这样的代码:用这样明显的方式来复活一个对象,除非是学术上的练习。但我看过这样的代码,析构函数试图完成一些实质的工作,最后还通过析构函数的调用把引用放到对象中,从而把自己复活。析构函数里面的代码看上去是精心设计的,另外还有处理函数里的。再检查一遍,这些代码是做了其它事情,而不是 释放资源!这些行为会为你的应用程序在后期的运行中产生很多BUG。删除这些方法,确保析构函数和Dispose()方法除了清理资源外,什么也不做。
在托管环境里,我们不需要为每一个类型都要创建一个终结器。只有当类型包含非托管类型,或者类所包含的成员实现了IDisposable接口,我们才需要为它们创建终结器。即使我们只需要IDisposable接口(而不需要终结器),我们也应该实现完整的模式。否则,派生类在实现标准Dispose模式时将变得非常复杂。我们应该遵循上述的Dispose模式,这会使得包括我们自己、我们类型的用户以及由我们的类型创建派生类的开发人员的生活变得更加轻松。
@IS2120#CNBLOGS.T2169364049[T1,L65,R1,V259]:备忘
$ € ₤ ₭ ₪ ₩ ₮ ₦ ₱ ฿ ₡ ₫ ﷼ ¥ ﷼ ₫ ₡ ฿ ₱ ₦ ₮ ₩ ₪ ₭ ₤ € $