在Using块的目标对象构造函数中捕获抛出的异常

5
using(SomeClass x = new SomeClass("c:/temp/test.txt"))
{
...
}

在 using 块内,将异常视为正常情况是可以的。但是如果 SomeClass 的构造函数可能会抛出异常怎么办?


1
为什么不要在try/catch块中包装using语句? - n535
这就是问题的全部意义。 - Mr. Boy
我在我的回答中解释了为什么使用块必须包含在try/catch块中。 - this. __curious_geek
首先,您应该尽量避免抛出异常的构造函数。在这种情况下,如果在构造函数中使用文件而不是文件路径,则会消除许多潜在的异常。 - Rune FS
1
@Rune FS:这是微软的语言推荐还是你个人的偏好?.NET类是否遵循这种规范? - Mr. Boy
@John。这是一个普遍的建议,但并不总是可能或可行的遵循。例如,当传递无效参数时,可以预期到参数异常。相当多的.NET类在传递无效参数时会抛出异常(但客户端代码可以轻松避免),而有些则会抛出其他异常。基本上,这是在构造函数中尽量少做的特殊情况。 - Rune FS
5个回答

5
将您的代码放入try catch中,例如:
try
{
   using(SomeClass x = new SomeClass("c:/temp/test.txt"))
   {
       ...
   }
}
catch(Exception ex)
{
   ...
}

这不是必要的。请看我的回答,了解原因。 - Oded
2
什么答案?我看不到任何一个。 - Mr. Boy

4
是的,当构造函数抛出异常时,这将是一个问题。你能做的就是在using块内部包含try/catch块。这就是为什么必须这样做的原因。 using块只是语法糖,编译器将每个using块替换为等效的try/finally块。唯一的问题是编译器不会将构造函数包装在try块中。编译后,你的代码在IL中将进行以下转换。
        //Declare object x of type SomeClass.
        SomeClass x;

        //Instantiate the object by calling the constructor.
        x = new SomeClass("c:/temp/test.txt");

        try
        {
            //Do some work on x.
        }
        finally
        {
            if(x != null)
                x.Dispose();
        }

从代码中可以看出,在构造函数抛出异常的情况下,对象x不会被实例化,如果没有被处理,控制权将不会从异常抛出点向后移动。
我昨晚在我的博客上发布了一篇关于这个主题的博客文章

我现在想知道为什么C#设计者没有在try块中包装对象构造,按照我的理解应该这样做。

编辑

我想我找到了答案,为什么C#没有将对象构建包装在生成的try块中以代替使用块。

原因很简单。如果你在try块中同时包装声明和实例化,则该对象将超出后面finally块的作用域,代码将无法编译,因为finally块几乎不存在该对象。如果只在try块中包装构造,并在try块之前保留声明,则即使在这种情况下,它也不会编译,因为发现你正在尝试使用已分配的变量。


你正在尝试使用一个已分配的变量。在生成try块之前,您可以先将其赋值为null,然后在try块内分配真实引用。但也许您不应该这样做,因为如果构造函数未能正确创建对象实例,则它有责任进行清理。 - Leonid Vasilev

1
我编写了一个快速测试程序来检查这一点,似乎在构造函数中抛出异常时Dispose方法不会被调用;
class Program
{
    static void Main(string[] args)
    {
        try
        {
            using (OtherClass inner = new OtherClass())
            {
                Console.WriteLine("Everything is fine");
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        Console.Read();
    }
}

class OtherClass : IDisposable
{
    public OtherClass()
    {
        throw new Exception("Some Error!");
    }

    void IDisposable.Dispose()
    {
        Console.WriteLine("I've disposed my resources");
    }
}

输出:

发生了一些错误!

如果您不抛出异常,那么...

输出:

一切都很好

我已经处理完资源

可能是因为对象从未被创建,所以没有需要调用Dispose。

如果构造函数已经分配了一些资源,并且之后发生了异常,需要进行清理以释放资源,但我不确定会发生什么。


0

如果一个类被设计得很好,这应该不是问题。记住总体的问题:

public class HoldsResources : IDisposable
{
    public HoldsResources()
    {
        // Maybe grab some resources here
        throw new Exception("whoops");
    }
}

using (HoldsResources hr = new HoldsResources())
{
}

所以,问题是,在抛出异常之前,你应该怎么处理HoldsResources构造函数分配的资源?
答案是,你不应该对这些资源做任何处理。那不是你的工作。当决定HoldsResources将持有资源时,就带来了正确释放它们的义务。这意味着在构造函数中使用try/catch/finally块,并且意味着正确实现IDisposable接口,在Dispose方法中释放这些资源。
你的责任是在使用实例完成后使用using块调用他的Dispose方法。仅此而已。

-1

当您在构造函数中获取不受垃圾回收管理的资源时,必须确保在出现问题时将其处理掉。

此示例显示了一个构造函数,可以在出现问题时防止泄漏,当您在工厂方法中分配可处理对象时,同样适用相同的规则。

class Sample
{
  IDisposable DisposableField;

  ...

  public Sample()
  {
    var disposable = new SomeDisposableClass();
    try
    {
       DoSomething(disposable);
       DisposableField = disposable;
    }
    catch
    {
       // you have to dispose of it yourself, because
       // the exception will prevent your method/ctor from returning to the caller.
       disposable.Dispose();
       throw;
    }
  }
}

编辑:我不得不将我的示例从工厂更改为构造函数,因为显然它并不像我希望的那样容易理解。(根据评论判断。)

当然,这样做的原因是:当您调用工厂或构造函数时,只能处理其结果。当调用进行时,您必须假设到目前为止一切都很好。

调用构造函数或工厂时,您无需进行任何反向心理分析以处理任何无法获取的内容。如果它确实引发异常,则由工厂/构造函数负责在重新抛出异常之前清除任何半分配的内容。 (希望这次足够详细了...)


@Robert Giesecke:我喜欢使用受保护的构造函数来实现可继承类,这将在构建对象时“偷偷摸摸”地复制一个对象;这些构造函数调用的委托应该被传递给基类工厂方法,该方法将在try-catch中包装它们。否则,如果派生类在其构造函数中抛出异常,就无法防止事件或资源泄漏。 - supercat
@supercat,也许是我吃太多晚餐了,但我不明白这怎么可能发生。如果派生的构造函数调用我的示例中的构造函数,它将成功或失败,但不会泄漏。不过,关于事件泄漏的您具体情况很有趣。 :-) - Robert Giesecke
1
@Robert Giesecke:当从“Sample”派生的类开始执行自己的构造函数时,Sample构造函数中的try-finally块将已经完成执行。如果此后发生异常,DisposableField将如何被清理?同样的,如果您有多个“var someDisposableField = New someDisposableClass();”声明,并且其中一个抛出异常,该怎么办?我有一种模式可以处理这个问题,但它依赖于调用受保护构造函数的工厂方法,该构造函数泄漏正在构建的对象。 - supercat
@Robert Giesecke: 我的模式允许将字段声明、初始化和清理一起处理。不幸的是,它只适用于vb.net;我认为在C#下它不太可能像在vb.net下那样干净,因为C#不允许初始化器引用正在构造的对象,也无法使用lambda表达式。可能可以通过线程静态变量来解决类似的问题,但那看起来很令人讨厌。 - supercat
1
@RobertGiesecke:如果在派生类的构造函数中抛出异常,这将阻止构造完成,那么基类对象使用的任何资源将通过什么方式进行清理?最接近可行的方法是将每个构造函数包装在try-finally块中,并具有“okay”标志,在失败的情况下,“finally”块调用this.Dispose(true)(要求Dispose方法处理部分构造对象)。可行,但有点棘手。我真的希望.NET能更好地处理这个问题。 - supercat
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接