为什么在C#中应使用IDisposable而不是using语句?

5
今天,我想对一个文件执行操作,所以我想到了这段代码。
    class Test1
    {
        Test1()
        {
            using (var fileStream = new FileStream("c:\\test.txt", FileMode.Open))
            {
                //just use this filestream in a using Statement and release it after use. 
            }
        }
    }

但在代码审查时,我被要求实现IDisposable接口和Finalizer方法

    class Test : IDisposable
    {
        Test()
        {
            //using some un managed resources like files or database connections.
        }

        ~Test()
        {
            //since .NET garbage collector, does not call Dispose method, i call in Finalize method since .net garbage collector calls this
        }

        public void Dispose()
        {
            //release my files or database connections
        }
    }

但是,我的问题是为什么我要这样做?

虽然我无法按照我的方式证明,但当使用语句本身可以释放资源时,为什么我们需要使用 IDisposable 呢?

有任何特定的优点或者我遗漏了某些东西吗?


5
如果你有需要清理的非托管资源,那么你应该添加一个终结器。FileStream是一种托管资源。 - Damien_The_Unbeliever
6个回答

9
在你的示例中,使用`using`语句是正确的,因为你只在方法的范围内使用资源。例如:
Test1()
{
    using (FileStream fs = new FileStream("c:\\test.txt", FileMode.Open))
    {
        byte[] bufer = new byte[256];
        fs.Read(bufer, 0, 256);
    }
}

但如果资源在一个方法之外被使用,那么您应该创建Dispose方法。 以下代码是错误的:
class Test1
{
    FileStream fs;
    Test1()
    {
        using (var fileStream = new FileStream("c:\\test.txt", FileMode.Open))
        {
            fs = fileStream;
        }
    }

    public SomeMethod()
    {
        byte[] bufer = new byte[256];
        fs.Read(bufer, 0, 256);
    }
}

正确的做法是实现 IDisposable ,以确保文件在使用后被释放。

class Test1 : IDisposable
{
    FileStream fs;
    Test1()
    {
        fs = new FileStream("c:\\test.txt", FileMode.Open);
    }

    public SomeMethod()
    {
        byte[] bufer = new byte[256];
        fs.Read(bufer, 0, 256);
    }

    public void Dispose()
    {
        if(fs != null)
        {
            fs.Dispose();
            fs = null;
        }
    }
}

太好了。如果资源在一个方法之外被使用,那么你应该创建Dispose方法。听起来是另一个很好的观点。非常感谢。 - now he who must not be named.

8
首先,需要说明一点,因为您似乎对usingIDisposable之间的交互方式有些困惑:之所以您可以说using (FileStream fileStream = Whatever()) { ... },正是因为FileStream类实现了IDisposable。您的同事建议您在您的类上实现IDisposable,以便您能够说using (Test test = new Test()) { ... }
值得一提的是,除非您想让FileStream在整个Test1实例的生命周期内保持打开状态,否则我认为您最初编写的代码方式要比建议的更好。这种情况可能发生的一个原因是文件可能会在Test1构造函数被调用后从某些其他来源进行更改,这样你就会陷入使用旧数据的境地。保持FileStream打开的另一个原因可能是如果您特别想在Test1对象存活期间防止其他地方对文件进行写入,那么您需要锁定该文件。
总的来说,尽早释放资源是一个好习惯,而您原始的代码似乎做到了这一点。我有些怀疑的一件事是工作是在您的构造函数中完成而不是在一些从外部明确调用的方法中完成(解释:http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/)。但这完全是另一回事,与是否使您的类实现IDisposable的问题无关。

5
根据您提供的信息,没有任何理由在Test上实现IDisposable或finalizer。
只有在释放非托管资源(窗口句柄、GDI句柄、文件句柄)时才需要实现finalizer。通常情况下,您不必这样做,除非您正在使用PInvoke Win32 API之类的东西。Microsoft已经为您包装了FileStream,因此您不必担心文件句柄。
当对象被垃圾回收时,finalizer用于清理非托管资源。
由于垃圾收集器在决定收集您的对象之前可能需要很长时间,因此您可能希望有一种方法来触发清理。不,GC.Collect()不是正确的方法。;)
为了允许早期清理本机资源而无需等待垃圾收集器,您可以在类上实现IDisposable。这使得调用者可以在不等待GC的情况下触发清理。这不会导致您的对象被GC释放。它所做的只是释放本机资源。
在一个对象拥有另一个可处理的对象的情况下,拥有对象也应该实现IDisposable并简单地调用其他对象的Dispose()
例如:
class Apple : IDisposable
{
    HWND Core;

    ~Apple() { Free(); }
    Free()
    {
        if(Core != null)
        {
            CloseHandle(Core); 
            Core = null;
        }
    }
    Dispose() { Free(); }
}

class Tree : IDisposable
{
    List<Apple> Apples;
    Dispose()
    {
        foreach(var apple in Apples)
            apple.Dispose();
    }
}

请注意,Tree 没有终结器。它实现了 Dispose,因为它必须关心 Apple 的清理。为了确保清理 Core 资源,Apple 有一个终结器。通过调用 Dispose(),我们可以使得 Apple 提前清理。
你的类 Test 不拥有任何未受控或 IDisposable 成员字段,所以你不需要 Dispose,也不需要终结器。你需要创建一个可被处理的资源 FileStream,但是你在离开方法之前清理了它,因此它不是 Test 对象的所有者。
这个情况有一个例外。如果你正在编写一个已知将被其他类继承并且其他类可能会实现 IDisposable 接口的类,那么你应该实现 IDisposable。否则,调用方将不知道如何处理对象(甚至无法处理,除非进行强制类型转换)。然而,这是一种不好的代码设计。通常情况下,你不会继承一个类并添加 IDisposable 接口。如果确实需要这样做,那么它就很可能是一个糟糕的设计。

1
我必须不同意你最后一个观点。(之前的都是正确的。)如果你的特定实例(或它所管理的实例)不管理非托管资源,那么就没有绝对的理由去实现IDisposable。考虑一个基本的Settings类,它知道如何添加和删除值,验证它们等等,但它没有存储的知识。现在你将其作为FileSettings或DBSettings的子类,它们各自使用非托管资源。即使基类没有使用这些资源,它们也应该实现IDisposable(除非它们在内部使用using语句使用这些资源)。 - Mark A. Donohoe
@MarqueIV 是的,添加 IDisposable 确实很糟糕,但这是不可避免的。这是由于里氏替换原则所致。如果您有一个 Settings 基类,那么任何接受 Settings 类型对象的消费者也必须接受 FileSettings 类型对象。现在,如果 FileSettings 类需要清理,消费者应该如何知道呢?这就是我在那个点提到类型转换的原因。如果 Settings 子类 可能 实现 IDisposable,那么拥有 Settings 对象的每个消费者都必须始终检查对象是否转换为 IDisposable - dss539
@MarqueIV 当你设计Settings基类时,你已经定义了该类的契约。每个方法都是该类的一种能力。IDisposable是特殊的,因为它还传达了调用代码的责任。在子类中添加新的责任是有风险的,因为那么所有操作Settings对象的人都必须神奇地知道某些子类可能需要的这种责任。最安全的方法就是将该责任添加到基类的契约中,以避免令消费者感到惊讶。 - dss539
1
我认为,如果释放资源不是基类的责任,那么接收该基类的任何地方都不应该知道它,因为这不是其契约的一部分,完全遵循了里氏替换原则。然而,子类的创建者应该知道,并且会知道,因为他们将创建一个FileSettings的实例。这就是为什么我必须坚持我的原始陈述,添加比不添加更不好闻。毕竟,我怎么知道谁会继承Settings并可能添加什么呢?难道我要把所有可能的东西都加进去吗?不需要。那为什么IDisposable又不同呢? - Mark A. Donohoe
@MarqueIV 你不必创建对象就可以使用对象。LSP的重点是消费者可以将每个子类视为基类相同。因此,您可以拥有一个包含所有设置的SettingsRepository,其中包含一个List<Settings>字段。现在,您需要SettingsRepository要么有一个单独的List<FileSettings>来跟踪这些设置,要么尝试将每个对象强制转换为IDisposable。但实际上会发生的是,编写SettingsRepository的人会假设他可以只使用基类作为合同。处理不会发生。 - dss539
显示剩余5条评论

5
"没有人"给出的答案是正确的,即using块只能用于实现IDisposable接口的类,并且其解释是完美的。你提出的问题是“为什么我需要在Test类上添加IDisposable?但在代码审查中,我被要求在Test类上实现IDisposable接口和Finalizer方法。”
答案很简单
1)按照许多开发人员遵循的编码标准,在使用某些资源的类上实现IDisposable总是有益的,一旦该对象的作用域结束,该类中的Dispose方法将确保所有资源都已被释放。
2)编写的类永远不会是未来不进行任何更改,如果进行此类更改并添加了新资源,则开发人员知道他必须在Dispose函数中释放这些资源。

4
从技术上讲,这是一个回答,但“未来变化”是一个可怕的理由。除非您有特定功能,并且相当确定将在其中实现,否则没有理由使一个类实现IDisposable。即使如此,您必须处于一种情况下,在该情况下无法更改类的接口,但更改实现不会产生破坏性变化。 - Joel McBeth
4
如果你的类使用IDisposable接口,那么将使用你的类的人必须假定它具有未受管控的资源。然后,他们必须将你的类的任何对象包含在一个“using”语句中(或调用dispose方法),以确保他们的代码不会泄漏未受管控的资源。如果每个人都这样做了,无论是否使用未受管控的资源,所有类的实例化都必须被包含在"using"语句中。当你的类没有使用未受管控的资源时,向你的类的用户撒谎并告诉他们你的类使用未受管控的资源是一个好主意吗? - David Rector
@DavidRector 我的第一点确实提到了“在使用某些资源的类上使用IDisposable的”... - Deepak Bhatia

2

为什么我们要使用IDisposable

简短的回答是,任何没有实现IDisposable的类都不能与using一起使用。

当使用语句本身可以释放资源时

不,它本身无法释放资源。

正如我之前所写,您需要实现IDisposable才能使用using。现在,当您实现IDisposable时,您将获得一个Dispose方法。在此方法中,您编写所有应在不再需要该对象时处理的所有需要处置的资源的代码。

USING的目的是当对象超出其范围时,它将调用dispose方法,就这样。

例子:

 using(SomeClass c = new SomeClass())
 { }

将会翻译为

 try
 {
     SomeClass c = new SomeClass();
 }
 finally
 {
     c.Dispose();
 }

2
但是,在Test1类中,我没有实现IDisposable接口,但仍然能够使用“using”关键字。 - now he who must not be named.
你在 FileStream 类上使用了 using,而不是 Test1 类。 - Ehsan
你正在使用 "using" 来处理 FileStream,但是你不能在类上使用 "using"。所以我猜这个评论的意思是你应该为 Test1 类实现 IDisposable 接口,这样你就可以写成:using(Test1 t = new Test1())。 - Giannis Paraskevopoulos
因为您在 FileStream 上使用了 'using',而没有在 Test1 上使用。 - SKull
你不能使用(Test1 t = new Test1()) {} 来实现这个。 - Ehsan

0

我认为这个问题更像是“我应该立即释放文件还是使用访问该文件的类的Dispose方法?”

这取决于:如果您仅在构造函数中访问文件,则我认为没有理由实现IDisposable。使用using是正确的方式。

否则,如果您在其他方法中也使用同一个文件,可能最好的做法是打开文件一次并确保在Dispose方法中关闭它(实现IDisposable)。


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