65.9K
CodeProject 正在变化。 阅读更多。
Home

RAII (资源获取即初始化) C# 辅助类

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.64/5 (14投票s)

2010年10月28日

CPOL

3分钟阅读

viewsIcon

48087

一些有用的 RAII C# 辅助类, 用于不实现 IDisposable 的功能。

引言

RAII 到底是什么?

RAII 代表 "资源获取即初始化 (Resource Acquisition Is Initialization)"。它是一种编程惯用法,确保不会发生资源泄漏(例如丢失的内存、打开的句柄、悬空的临界区监视器等),即使抛出异常也是如此。 对于像 C# 这样的语言,丢失的内存不是问题,但“打开的句柄”和“临界区监视器”的问题同样重要。

某些语言提供了一种机制,可以在对象实例的生命周期结束时确定性隐式地调用一个特定的函数(例如 C++ 中的析构函数)。 C++ 中的 RAII 惯用法利用了这一事实,并将资源的“获取”放置在构造函数(= 初始化)中,并在析构函数中释放资源。

例如,C++ 自动指针以及任何 guard 或 sentry 类都在实现 RAII 惯用法。 有关更多信息,请参见 RAII 惯用法

在 C++ 中,报告 sentry(RAII 类)会在其构造函数中记录文本,并在其析构函数(在离开函数体时调用)中再次记录一些文本。

// this is C++ use of an RAII class
void f()
{
   ReportSentry reportSentry("function f");
   ...
}

C# 不知道这种确定性和隐式析构函数调用,这就是我解释如何在 C# 中处理此问题的原因。

C# 世界中的 RAII

来自 C++ 背景,我经常应用 RAII 惯用法。 这不仅用于资源,还用于任何需要对称“打开-关闭”、“添加-删除”、“设置-重置”、“打开标签-关闭标签”、“进入临界区-离开临界区”、“报告函数开始-报告函数结束”等操作的任何类型的操作——无论控制流如何离开当前作用域。

C# 提供了众所周知的 IDisposable 接口。 每个实现此接口的类都可以被认为是“已准备好进行 RAII”,也就是说,可以在...中使用此类的实例。

using (...) { ... }

...语句。

但是,如果您需要 RAII,但没有用于 using (...) { ... } 语句的类,该怎么办?

本文展示了一些简单的助手类,这些类为 init 和 cleanup 委托提供了一个 IDisposable 包装器类。

当然,这根本不是火箭科学——实际上,它非常简单。 它应该有助于清除涉及的异常处理的迷雾,并专注于正常的控制流程。

Using the Code

首先,我并排展示 RAII 的两种用法与手工制作的用法。

具有 RAII 支持类 手工制作
using (var objGuard = new RAIIGuard<int>(
                  () =>objStack.Pop(),
                  (e)=>objStack.Push(e)))
{
    ...
    if (objGuard.Item != 0)
    {
        return;
    }    
    ...
    if (...)
    {
         throw new ...;
    }
    ...
}
int iTaken;
bool bTaken = false;
try
{
    // access the taken object
    iTaken = objStack.Pop();
    bTaken = true; 
    ...
    if (iTaken == 0)
    {
        return;
    }    
    ...
    if (...)
    {
         throw new ...;
    }
    ...
}
finally
{
    if (bTaken)
    {
        objStack.Push(iTaken);
    }
}

我认为 RAII 支持的方法更清晰。 如果 lambda 表达式让您感到困惑,您可以始终使用委托或方法,例如

private int Pop() { return Stack.Pop(); }
private void Push(int i) { Stack.Push(i); }
using (var objGuard = new RAIIGuard<int>(Pop, Push)) 
{
   ...
}

方便地,有两个类来提供该功能

  1. 一个具有 init 操作和 cleanup 操作(例如,登录,注销)
  2. 一个具有 init 函数,返回一个对象,cleanup 函数使用该对象(例如,使用会话对象进行登录和注销)
// used for symmetric actions like login, logout
public sealed class RAIIGuard: IDisposable
{
    private Action Cleanup { get; set; }
    public RAIIGuard(Action init, Action cleanup)
    {
        Cleanup = cleanup;
        if (init != null) init();
    }
    void IDisposable.Dispose() { if (Cleanup != null) Cleanup(); }
]
// used for symmetric actions that must pass
// over an object from init to cleanup and that
// need to provide the item to the "using" body
public sealed class RAIIGuard<T>: IDisposable
{
    private Action<T> Cleanup { get; set; }
    public T Item { get; private set; }
    public RAIIGuard(Func<T> init, Action<T> cleanup)
    {
        Cleanup = cleanup;
        Item = (init != null) ? init() : default(T);
    }
    void IDisposable.Dispose() { if (Cleanup != null) Cleanup(Item); }
]

结论

如果“守护” C# 语言的委员会可以重新考虑实现本机 RAII 支持(例如,可以声明一个 guard 作为某种“值”类型的实例,并且在作用域结束时,隐式和确定性地调用“Dispose”方法),那将仍然很好。

上述方法仍然需要客户端的某种程度的合作,而纯 RAII 解决方案不需要客户端的合作,也就是说,在 C# 中,仍然可以通过不调用 Dispose() 方法来破坏 RAII 惯用法 :-( 。

与此同时,上面显示的方法对我来说已经足够好了...

历史

  • 2010-10-27 初始版本
  • 2010-10-27 修复了一些错字
  • 2010-10-29 添加了有关 RAII 的更多背景信息,Dispose() 的显式实现(感谢 Roberto Collina)
© . All rights reserved.