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

Should I Treat Entity Framework as an Unmanaged Resource?

我正在使用 class,它在其构造函数中使用了对 EF 的引用。

我已经实现了 IDisposable,但我不确定是否需要析构函数,因为我不确定是否可以 class将 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 是非托管的,那么我会这样做:

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);

    }
}

是哪一个?

在这种情况下,您绝对不想使用终结器(析构函数)。

DbContext 是否包含非托管资源,甚至它是否负责任地释放这些非托管资源,与您是否可以尝试从终结器调用 DbContext.Dispose() 无关。

事实是,任何时候你有一个托管对象(DbContext 的一个实例),它是 never 可以安全地尝试调用该实例上的任何方法。原因是,在调用终结器时,DbContext 对象可能已经被 GC 收集并且不再存在。如果发生这种情况,您将在尝试调用 Db.Dispose() 时得到 NullReferenceException。或者,如果幸运的话,Db 仍然“活着”,如果 DbContext.Dispose() 方法依赖于其他已完成和收集的对象,也可以从 DbContext.Dispose() 方法中抛出异常。

正如这个 "Dispose Pattern" MSDN article 所说:

X DO NOT access any finalizable objects in the finalizer code path, because there is significant risk that they will have already been finalized.

For example, a finalizable object A that has a reference to another finalizable object B cannot reliably use B in A’s finalizer, or vice versa. Finalizers are called in a random order (short of a weak ordering guarantee for critical finalization).

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

Myth: Finalizers run in a predictable order

Suppose we have a tree of objects, all finalizable, and all on the finalizer queue. There is no requirement whatsoever that the tree be finalized from the root to the leaves, from the leaves to the root, or any other order.

Myth: An object being finalized can safely access another object.

This myth follows directly from the previous. If you have a tree of objects and you are finalizing the root, then the children are still alive — because the root is alive, because it is on the finalization queue, and so the children have a living reference — but the children may have already been finalized, and are in no particularly good state to have their methods or data accessed.


还有一点需要考虑:您要处理什么?您是否关心确保及时关闭数据库连接?如果是这样,那么您会对 EF documentation 对此有何评论感兴趣:

By default, the context manages connections to the database. The context opens and closes connections as needed. For example, the context opens a connection to execute a query, and then closes the connection when all the result sets have been processed.

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


最后,您可能要记住的最后一件事是,您发布的代码没有终结器,因为您在另一个 [= 的构造函数中实例化了 DbContext 84=],实际上 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;
    }
}

假设您的 class 是这样使用的:

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

尽管这看起来是一个非常安全和干净的设计,因为在 ExampleClass after[= 的构造函数中抛出异常79=] DbContext 的新实例已经创建,ExampleClass.Dispose() 永远不会被调用,并且推而广之,DbContext.Dispose() 也永远不会在新创建的实例上被调用。

您可以阅读更多关于这种不幸情况的信息here

为了确保始终调用 DbContextDispose() 方法,无论 ExampleClass 构造函数内部发生什么,您都必须修改 ExampleClass class 像这样:

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.

的实例,那么以上内容实际上只是一个问题。

C# 提供垃圾收集,因此不需要显式析构函数。但是,如果您确实控制了非托管资源,则需要在使用完该资源后显式释放该资源。通过 Finalize( ) 方法(称为终结器)提供对该资源的隐式控制,当您的对象被销毁时,垃圾收集器将调用该方法。

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