考虑以下课程:
class C1 : IDisposable {...}
class C2 : IDisposable {...}
sealed class C3 : IDisposable
{
public C3()
{
c1 = new C1();
throw new Exception(); //oops!
}
~C3()
{
//What can we do???
}
public void Dispose()
{
if ( c1 != null ) c1.Dispose();
if ( c2 != null ) c2.Dispose();
}
private C1 c1;
private C2 c2;
//assume that this class does not contains native resources
}
现在,假设我们正确使用一次性物品:
using (var c3 = new C3())
{
}
此代码段怎么样?
在这种情况下,我们不能调用Dispose方法,因为我们的对象永远不存在。
我们知道,在这种情况下会调用终结器,但是我们只能处理CriticalFinilizedObjects而我们不能处理C1或C2对象。
我的解决方案非常简单:
sealed class C3 : IDisposable
{
public C3()
{
try {
c1 = new C1();
throw new Exception(); //oops!
c2 = new C2();
}
catch(Exception)
{
DisposeImpl();
throw;
}
}
~C3()
{
//Not deterministically dispose detected!
//write to log!
//Invalid class usage. Or not??
}
public void Dispose()
{
DisposeImpl();
}
private void DisposeImpl()
{
if ( c1 != null ) c1.Dispose();
if ( c2 != null ) c2.Dispose();
GC.SuppressFinalize(this); //all resources are released
}
private C1 c1;
private C2 c2;
}
此解决方案在某些细节上可能有所不同,但我认为您可以理解关键原则:如果构造函数抛出异常,我们会强制释放获取的资源并抑制最终化。
还有其他任何想法,建议或更好的解决方案吗?
P.S。 Herb Sutter在他的博文(http://herbsutter.wordpress.com/2008/07/25/constructor-exceptions-in-c-c-and-java/)中提出了这个问题,但他没有提出解决方案。
答案 0 :(得分:8)
你提出的是我得出的结论when thinking about this issue。
总之,在构造函数中执行尽可能多的工作以使对象处于可用状态,但如果无法成功完成,则清理在异常处理程序中分配的任何昂贵的托管资源。
在.NET中使用构造函数将对象置于可以使用的状态是惯用的。有些人会建议使用一个简单的构造函数,后跟一个Initialize
方法,其中任何“真正的”工作都是为了使对象处于正确的状态,但是我想不出一个框架类可以做到这一点,所以它不是.NET开发人员可以遵循的直观模式,因此不应该在.NET中完成,无论它是否是其他语言和平台的合理约定。
我认为这是一个相当罕见的案例 - 通常一个包装一次性资源的类会将它作为构造函数参数而不是自己创建它。
答案 1 :(得分:0)
更好的解决方案是不在构造函数中执行 ANY 逻辑。只需创建对象,就可以了。如果你真的需要在构造函数中做一些事情,请用try catch finally语句封装它,释放非托管资源。
答案 2 :(得分:0)
您实际上是不是解构主义者?
您是否尝试基本使用RAII?
我的整个程序
using System;
namespace Testing
{
class C1 : IDisposable
{
public C1()
{
}
public void Dispose()
{
Console.WriteLine( "C1 Destroyed" );
}
}
class C2 : IDisposable
{
public C2()
{
throw new Exception();
}
public void Dispose()
{
Console.WriteLine( "C2 Destroyed" );
}
}
class C3 : IDisposable
{
C1 c1;
C2 c2;
public C3()
{
try {
c1 = new C1();
c2 = new C2();
} catch {
this.Dispose();
throw new Exception();
}
}
~C3()
{
this.Dispose();
}
public void Dispose()
{
// basically an early deconstructor
Console.WriteLine( "C3 Being Destroyed" );
if ( c1 != null )
c1.Dispose();
if ( c2 != null )
c2.Dispose();
GC.SuppressFinalize(this);
Console.WriteLine( "C3 Destroyed" );
}
}
class MainClass
{
public static void Main(string[] args)
{
try {
using ( var c3 = new C3() )
{
Console.WriteLine("Rawr");
}
} catch {
Console.WriteLine( "C3 Failed" );
}
GC.Collect();
}
}
}
答案 3 :(得分:0)
首先在构造函数参数上断言。断言任何后续逻辑都不会抛出异常。这是断言构造函数逻辑中使用的任何数据对于该特定用法都是有效的。像这样:
c3(string someString)
{
Debug.Assert(!string.IsNullOrEmpty())
c1 = new c1(someString);
}
如果空字符串会导致c1抛出异常。
如果您无法确保输入验证不会抛出任何异常。尽可能重写代码。因为那将是一种非常强烈的气味。如果异常抛出代码不是用户而是供应商更改供应商。这不会是你的代码最后遇到的问题。
答案 4 :(得分:0)
在“try”中包装构造函数的主体并在失败时调用Dispose可能就像你能做的那样好。但是,我建议使用“try-Finally”,而不是“Try-Catch”,并在“try”结束时设置“ok”标志。如果在构造函数期间抛出某些东西,那么调试器将有机会在抛出时查看系统状态。
在vb.net中,可以做得更好一些,因为vb.net支持异常过滤,并且因为vb.net在基类构造函数之后运行字段初始化器,因此允许在内部使用基类成员derived-class字段初始值设定项。
请参阅我的问题Handling iDisposable in failed initializer or constructor以及我对它的回答以获取更多信息。