我应该把Entity Framework视为非托管资源吗?

8

我正在使用一个在构造函数中引用了 EF 的类。

我已经实现了 IDisposable,但我不确定是否需要一个析构函数,因为我不确定能否将 EF 归类为非托管资源。

如果 EF 是托管资源,那么我就不需要一个析构函数,因此我认为这是一个正确的例子:

public ExampleClass : IDisposable
{
    public ExampleClass(string connectionStringName, ILogger log)
    {
        //...
        Db = new Entities(connectionStringName);
    }

    private bool _isDisposed;

    public void Dispose()
    {
        if (_isDisposed) return;

        Db.Dispose();

        _isDisposed= true;
    }
}

如果EF是非托管的,则我会选择这个:

如果EF是非托管的,那么我会选择这个:

public ExampleClass : IDisposable
{
    public ExampleClass(string connectionStringName, ILogger log)
    {
        //...
        Db = new Entities(connectionStringName);
    }

    public void Dispose()
    {
        Dispose(true);
    }

    ~ExampleClass()
    {
        Dispose(false);
    }

    private bool _isDisposed;

    protected virtual void Dispose(bool disposing)
    {
        if (_isDisposed) return;

        // Dispose of managed resources
        if (disposing)
        {
            // Dispose of managed resources; assumption here that EF is unmanaged.
        }
        // Dispose of unmanaged resources
        Db.Dispose();

        _isDisposed = true;
        //freed, so no destructor necessary.
        GC.SuppressFinalize(this);

    }
}

这是哪一个?


IDisposable 不仅仅处理非托管资源。 - CodeCaster
1
我会始终控制DbContext的创建和销毁。它需要为一个小的工作单元进行创建。它的内部遵循“工作单元”模式。 - Callum Linington
我认为它是托管的,并期望它实现自己的终结器来处理其未托管的部分。 - James Barrass
1
问题在于“DbContext是否为非托管?”没有明确的“是”或“否”的答案。它是一个CLR类,因此肯定是托管对象。然而,在后台,它使用来自池的数据库连接,这些连接往往是(托管包装器)非托管资源。但是,默认情况下,DbContext和相关的内部类会释放这些非托管资源。当您不打开连接时,仅处理DbContext就足够了(通常甚至不需要)。没有其他事情要做。 - CodeCaster
2
@CodeCaster - 谢谢你。然而,DbContext和相关的内部类默认会自行释放这些非托管资源。这似乎很有道理,让我想把EF视为托管资源。据我所知,对于托管对象,我不需要使用析构函数,因此在得到其他通知之前,我可以放心地使用第一个示例。 - jacoblambert
显示剩余10条评论
2个回答

9
在这种情况下,您永远不会想使用终结器(析构函数)。
无论DbContext是否包含非托管资源,甚至无论它是否负责释放这些非托管资源,都与您是否可以尝试从终结器调用DbContext.Dispose()无关。
事实上,每当您拥有托管对象(例如DbContext的实例)时,尝试在该实例上调用任何方法都是不安全的。原因是,在终结器被调用时,DbContext对象可能已经被垃圾回收并不存在了。如果发生这种情况,调用Db.Dispose()时会出现NullReferenceException。或者,如果您很幸运,而Db仍然“存在”,则异常也可能从DbContext.Dispose()方法内部抛出,如果它依赖于其他已被终结和回收的对象。

正如"Dispose Pattern" MSDN article所述:

不要在终结器代码路径中访问任何可终结对象,因为它们很可能已经被终结。

例如,一个具有对另一个可终结对象B的引用的可终结对象A不能可靠地在A的终结器中使用B,反之亦然。 终结器按随机顺序调用(除了关键终结的弱排序保证)。

此外,请注意来自Eric Lippert的When everything you know is wrong, part two的以下内容:

神话:finalizer按照可预测的顺序运行

假设我们有一个对象树,所有对象都是finalizable,并且都在finalizer队列上。没有任何要求树从根到叶子、从叶子到根或其他任何顺序进行finalize。

神话:正在finalize的对象可以安全地访问另一个对象。

这个神话直接源于前面的神话。如果您有一个对象树,并且您正在finalize根,则子对象仍然存活——因为根仍然存活,因为它在finalization队列上,所以子对象具有存活的引用——但是子对象可能已经被finalize,并且处于不好的状态以让其方法或数据被访问。


还有一些需要考虑的事情:你想要处理什么?你是否担心数据库连接及时关闭?如果是这样,那么你会对 EF文档中关于此事的内容感兴趣:

默认情况下,上下文管理与数据库的连接。上下文根据需要打开和关闭连接。例如,上下文打开一个连接来执行查询,然后在处理完所有结果集后关闭连接。

这意味着,默认情况下,连接不需要调用DbContext.Dispose()来及时关闭。它们会在执行查询时从连接池中打开和关闭。因此,虽然始终明确地调用DbContext.Dispose()仍然是一个非常好的主意,但知道如果由于某种原因你没有这样做或忘记了,这不会导致某种类型的连接泄漏也是很有用的。


最后,你需要记住的一件事是,如果使用你提供的代码且没有使用终止器,由于在另一个类的构造函数中实例化了 DbContext,所以 DbContext.Dispose() 方法可能不会被调用。了解这种特殊情况非常重要,这样你就不会因此遭受损失。
例如,假设我稍微调整一下你的代码,使得异常可以在构造函数中实例化 DbContext 之后被抛出:
public ExampleClass : IDisposable
{
    public ExampleClass(string connectionStringName, ILogger log)
    {
        //...
        Db = new Entities(connectionStringName);
        
        // let's pretend I have some code that can throw an exception here.
        throw new Exception("something went wrong AFTER constructing Db");
    }

    private bool _isDisposed;

    public void Dispose()
    {
        if (_isDisposed) return;

        Db.Dispose();

        _isDisposed= true;
    }
}

假设您的类被使用如下:

using (var example = new ExampleClass("connString", log))
{
    // ...
}

尽管这看起来是一个完全安全和干净的设计,因为在 ExampleClass 构造函数内部抛出异常 之后 已经创建了 DbContext 的新实例,所以 ExampleClass.Dispose() 从未被调用,进而也从未调用新创建实例的 DbContext.Dispose()
您可以在 这里 阅读更多关于这种不幸情况的内容。
为确保无论 ExampleClass 构造函数中发生什么情况,DbContextDispose() 方法总是被调用,您需要将 ExampleClass 类修改为以下内容:
public ExampleClass : IDisposable
{
    public ExampleClass(string connectionStringName, ILogger log)
    {
        bool ok = false;
        try 
        {
            //...
            Db = new Entities(connectionStringName);
            
            // let's pretend I have some code that can throw an exception here.
            throw new Exception("something went wrong AFTER constructing Db");
            
            ok = true;
        }
        finally
        {
            if (!ok)
            {
                if (Db != null)
                {
                    Db.Dispose();
                }
            }
        }
    }

    private bool _isDisposed;

    public void Dispose()
    {
        if (_isDisposed) return;

        Db.Dispose();

        _isDisposed= true;
    }
}

但是上述问题只有在构造函数不仅仅创建DbContext实例时才存在。


非常感谢您的回复。不过,也许我们可以通过评论详细说明一些问题:首先,您能解释一下为什么EF是托管对象吗?我天真地认为EF是一个围绕着数据库连接的包装器(当然还有更多,但在这个上下文中只有这么多)。我假设它被管理的方式与我假设包装文件流的类是托管对象的方式相同(它是用C#编写的,因此受CLR控制)。流本身是由Win32 dlls打开的,也许我不知道,但它不是由CLR管理的。您回答了我的问题,但您没有告诉我为什么EF是非托管对象;我是对的吗? - jacoblambert
其次,你说过查询时上下文会从连接池中打开和关闭。因此,虽然显式调用DbContext.Dispose()仍然是一个非常好的做法,但如果出于某种原因你不这样做或者忘记了,那么默认情况下这并不会导致某种类型的连接泄漏。这缓解了我的疑虑。起初我认为需要在我的包装类上实现IDisposable主要是因为我担心EF可能处于未受管控状态,但我知道IDisposable被正确地用于释放两种资源。所以这一部分也很感激。 - jacoblambert
最后,感谢您考虑给予奖金,关于构造函数中的异常。这是我没有考虑到的一个要点,也是我没有意识到的模式的微妙之处。 - jacoblambert
1
关于你最初的评论,听起来你理解得很正确。所有.NET对象都是受管理的并且受垃圾回收影响的,包括 DbContext。相比之下,未受管理的资源通常是像文件句柄这样的东西,在使用低级winapi函数关闭。你应该只在清除未受管理的资源时定义终结器,基本上几乎从不这样做。 - sstan
很好,我明白不使用析构函数来管理资源,这就是为什么我在我的示例中没有这样做的原因。但我感谢您提醒其他人。 - jacoblambert

0
C# 提供垃圾回收机制,因此不需要显式析构函数。但是,如果您控制着一个非托管资源,那么在使用完它后,您需要显式地释放该资源。通过 Finalize() 方法(称为终结器),可以隐式地控制此资源,当对象被销毁时,垃圾回收器将调用该方法。

https://www.oreilly.com/library/view/programming-c/0596001177/ch04s04.html


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