拿2:使用IDisposable和“使用”作为获取“范围行为”的手段是否滥用?

时间:2013-06-26 13:24:44

标签: c# exception-handling anti-patterns

TL; DR - 在IDisposable.Dispose中执行业务逻辑是否合适?

在我寻找答案的过程中,我仔细阅读了这个问题:Is it abusive to use IDisposable and "using" as a means for getting "scoped behavior" for exception safety?它非常接近于解决这个问题,但我想把它解决掉。我最近遇到了一些看起来像这样的代码:

class Foo : IDisposable
{
    public void Dispose()
    {
        ExecuteSomeBusinessBehavior();
        NormalCleanup();
    }
}

用于以下语境:

try 
{
    using (var myFoo = new Foo())
    {
        DoStuff();
        foo.DoSomethingFooey();
        ...
        DoSomethingElse();
        Etc();
    }
}
catch (Exception ex)
{
    // Handle stuff
}

看到这段代码后,我立即开始发痒。这是我看到这段代码时看到的内容:

首先,仅查看使用情况,当代码离开使用范围时,实际的业务逻辑(而不仅仅是清理代码)将会被执行,这一点并不明显。

其次,如果“using”范围内的任何代码抛出异常,则Dispose方法中的业务逻辑仍将执行,并且在Try / Catch可以处理异常之前执行此操作。

我对StackOverflow社区的疑问是:将业务逻辑放在IDisposable.Dispose方法中是否有意义?是否有一种模式可以实现类似的结果而不会让我感到痒?

4 个答案:

答案 0 :(得分:5)

(对不起,这更多是评论,但超出了评论长度限制。)

实际上,在.NET框架中有一个示例,其中IDisposable用于创建范围,而执行有用的工作在处置时: TransactionScope

引用TransactionScope.Dispose

  

调用此方法标记事务范围的结束。如果TransactionScope对象创建了事务并且在作用域上调用了Complete,则在调用此方法时,TransactionScope对象将尝试提交事务。

如果您决定采取这种方式,我会建议

  • 你明确表示你的对象创建了一个范围,例如,通过调用FooScope代替Foo

  • 您认为当异常导致代码离开您的范围时应该会发生什么。在TransactionScope中,在块结尾处调用Complete的模式可确保Dispose可以区分这两种情况。

答案 1 :(得分:3)

IDisposable的真正含义是一个对象知道某个东西,某个地方已被置于一个应该被清理的状态,并且它具有执行这种清理所需的信息和动力。虽然与IDisposable相关联的最常见的“状态”是文件打开,分配的非托管图形对象等等,但这些只是使用的示例,而不是“正确”使用的定义。

IDisposableusing用于作用域行为时要考虑的最大问题是Dispose方法无法区分从{using引发异常的情况1}}阻止它正常退出的那些。这是不幸的,因为在很多情况下,根据退出是正常还是异常,确保具有两个退出路径之一的范围行为是有用的。

例如,考虑一个读写器锁定对象,其方法在获取锁定时返回IDisposable“标记”。很高兴地说:

using (writeToken = myLock.AcquireForWrite())
{
   ... Code to execute while holding write lock
}

如果没有try / catch或try / finally锁定手动编码锁的获取和释放,则在保持锁定时抛出的异常会导致等待锁的任何代码永远等待。这是一件坏事。如上所示使用using块将导致在块退出时释放锁定,无论是正常还是通过异常。不幸的是,这可能是一件坏事。

如果在保持写锁定时抛出意外异常,最安全的行为方式是使锁定无效锁定,以便任何当前或将来尝试获取锁定将立即生成锁定例外。如果程序无法在没有锁定资源可用的情况下继续进行,则此类行为将导致程序快速关闭。如果它可以继续,例如通过切换到某个备用资源,使资源无效将使其能够比无用地获取锁更有效地继续使用。不幸的是,我不知道有什么好的模式可以实现这一目标。人们可以这样做:

using (writeToken = myLock.AcquireForWrite())
{
   ... Code to execute while holding write lock
   writeToken.SignalSuccess();
}

并且如果在成功发出信号之前调用了令牌,Dispose方法会使令牌无效,但是如果发生成功的意外失败可能导致资源无效,而不提供有关发生的位置或原因的指示。如果代码在没有调用Dispose的情况下退出using块,那么SignalSuccess方法会抛出异常可能会很好,除非因为某些其他异常而退出时抛出异常会破坏所有信息关于那个其他例外,Dispose无法确定哪种方法适用。

考虑到这些因素,我认为最好的选择可能是使用类似的东西:

using (lockToken = myLock.CreateToken())
{
   lockToken.AcquireWrite(Describe how object may be invalid if this code fails");
   ... Code to execute while holding write lock
   lockToken.ReleaseWrite();
}

如果代码在没有调用ReleaseWrite的情况下退出,则尝试获取锁的其他线程将收到包含指示消息的异常。无法正确手动配对AcquireWriteReleaseWrite将使锁定的对象无法使用,但不会让其他代码等待它变得可用。请注意,不平衡的AcquireRead不必使锁对象无效,因为读取内的代码永远不会将对象置于无效状态。

答案 2 :(得分:2)

业务逻辑代码绝不应该在任何情况下写入Dispose方法的原因是,你依赖的是不可靠的路径。如果用户没有打电话给你的处理方法怎么办?你错过了一个完整的功能?如果您的dispose方法的方法调用中抛出了异常怎么办?当用户要求处置对象本身时,为什么要执行业务操作。从逻辑上讲,技术上不应该这样做。

答案 3 :(得分:0)

我目前正在通过Introduction to Rx阅读Lee Campbell,并且有一章名为IDisposable,他明确提倡利用与{{1}的整合构造,以便"创建瞬态范围"。

该章的一些关键引文:

  

"如果我们认为我们可以使用IDisposable接口来有效地创建一个范围,你可以创建一些有趣的小类来利用它。"

     

(...见下面的例子......)

     

"因此我们可以看到您可以使用IDisposable接口,而不仅仅是确定性地释放非托管资源的常见用法。它是管理任何事物的生命周期或范围的有用工具;从秒表计时器,到控制台文本的当前颜色,到订阅一系列通知。

     

Rx库本身采用了IDisposable接口的自由使用,并引入了几个自己的自定义实现:

     
      
  • BooleanDisposable
  •   
  • CancellationDisposable
  •   
  • CompositeDisposable
  •   
  • ContextDisposable
  •   
  • MultipleAssignmentDisposable
  •   
  • RefCountDisposable
  •   
  • ScheduledDisposable
  •   
  • SerialDisposable
  •   
  • SingleAssignmentDisposable"
  •   

他给出了两个有趣的小例子:

示例1 - 定时代码执行。 "这个方便的小类允许您创建范围并测量代码库的某些部分运行的时间。"

using

输出:

public class TimeIt : IDisposable
{
    private readonly string _name;
    private readonly Stopwatch _watch;

    public TimeIt(‌string name)
    {
        _name = name;
        _watch = Stopwatch‌.StartNew(‌);
    }

    public void Dispose(‌)
    {
        _watch‌.Stop(‌);
        Console‌.WriteLine(‌"{0} took {1}", _name, _watch‌.Elapsed);
    }
}

using (‌new TimeIt(‌"Outer scope"))
{
    using (‌new TimeIt(‌"Inner scope A"))
    {
        DoSomeWork(‌"A");
    }

    using (‌new TimeIt(‌"Inner scope B"))
    {
        DoSomeWork(‌"B");
    }

    Cleanup(‌);
}

示例2 - 暂时更改控制台文本颜色

Inner scope A took 00:00:01.0000000
Inner scope B took 00:00:01.5000000
Outer scope took 00:00:02.8000000

输出:

//Creates a scope for a console foreground color‌. When disposed, will return to 
//  the previous Console‌.ForegroundColor

public class ConsoleColor : IDisposable
{
    private readonly System‌.ConsoleColor _previousColor;

    public ConsoleColor(‌System‌.ConsoleColor color)
    {
        _previousColor = Console‌.ForegroundColor;
        Console‌.ForegroundColor = color;
    }

    public void Dispose(‌)
    {
        Console‌.ForegroundColor = _previousColor;
    }
}


Console‌.WriteLine(‌"Normal color");

using (‌new ConsoleColor(‌System‌.ConsoleColor‌.Red))
{
    Console‌.WriteLine(‌"Now I am Red");

    using (‌new ConsoleColor(‌System‌.ConsoleColor‌.Green))
    {
        Console‌.WriteLine(‌"Now I am Green");
    }

    Console‌.WriteLine(‌"and back to Red");
}