取2:使用IDisposable和“using”作为获取“作用域行为”的手段是否滥用

本文关键字:段是否 是否 作用域行为 获取 IDisposable 使用 using 作用域 | 更新日期: 2023-09-27 18:20:02

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

在寻找答案的过程中,我通读了这个问题:使用IDisposable和";使用";作为获得";作用域行为";为了异常安全?它非常接近于解决这个问题,但我想彻底攻击它。我最近遇到了一些代码,看起来像这样:

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方法中有意义吗?有没有一种模式能在不让我发痒的情况下达到类似的效果?

取2:使用IDisposable和“using”作为获取“作用域行为”的手段是否滥用

(抱歉,这只是一条评论,但它超过了评论长度限制。)

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

引用TransactionScope.Dispose:

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

如果你决定走那条路,我建议

  • 明显地,您的对象创建了一个作用域,例如,将其称为FooScope而不是Foo

  • 当异常导致代码离开您的作用域时,您会仔细考虑应该发生什么。在TransactionScope中,在块末尾调用Complete的模式确保Dispose能够区分这两种情况。

IDisposable的真正含义是,一个对象知道某个东西,某个地方已经进入了应该清理的状态,并且它拥有执行这种清理所需的信息和动力。尽管与IDisposable相关的最常见的"状态"是打开的文件、分配的非托管图形对象等。这些只是使用的示例,而不是"正确"使用的定义。

使用IDisposableusing进行作用域行为时要考虑的最大问题是,Dispose方法无法区分从using块引发异常的场景和正常退出的场景。这是不幸的,因为在许多情况下,根据退出是正常的还是异常的,确定作用域的行为被保证具有两个退出路径中的一个是有用的。

例如,考虑一个读写器锁对象,该对象的方法在获取锁时返回IDisposable"令牌"。最好说:

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

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

如果在持有写锁时引发意外异常,则最安全的做法是使锁失效,这样任何当前或未来获取锁的尝试都会立即引发异常。如果程序在锁定的资源不可用的情况下无法有效地进行,这种行为将导致它迅速关闭。如果它可以继续进行,例如切换到某个备用资源,那么使资源无效将使它能够比无用地获取锁更有效地继续进行。不幸的是,我不知道有什么好的模式可以做到这一点。一个人可以做这样的事情:

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

如果在发出成功信号之前调用了Dispose方法,则使其无效,但发出成功信号的意外失败可能导致资源无效,而不提供发生这种情况的位置或原因的指示。如果代码正常退出using块而不调用SignalSuccess,则让Dispose方法抛出异常可能是好的,只是当它因其他异常而退出时抛出异常会破坏有关该其他异常的所有信息,并且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不必使锁对象无效,因为读取中的代码永远不会使对象处于无效状态。

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

我目前正在阅读Lee Campbell的《Rx导论》,其中有一章名为IDisposable,他明确主张利用与using结构的集成;创建瞬态范围";。

该章中的一些关键引文:

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

(…参见以下示例…)

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

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

  • Boolean一次性
  • 取消一次性
  • Composite一次性
  • 上下文一次性
  • 多次分配一次性
  • RefCount一次性
  • Scheduled一次性
  • 系列一次性
  • SingleAssignmentDisposable

他举了两个有趣的小例子:


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

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(‌);
}

输出:

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

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

//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");
}

输出:

Normal color
Now I am Red
Now I am Green
and back to Red