为什么在释放后使用已释放的对象不会抛出异常?

17

调用已释放对象的方法是否合法?如果是,为什么?

在下面的演示程序中,我有一个可处理的类 A(它实现了 IDisposable 接口)。据我所知,如果我将可处理对象传递给 using() 构造,则 Dispose() 方法会在关闭括号时自动调用:

A a = new A();
using (a)
{
   //...
}//<--------- a.Dispose() gets called here!

//here the object is supposed to be disposed, 
//and shouldn't be used, as far as I understand.

如果这是正确的,那么请解释一下这个程序的输出:
public class A : IDisposable
{
   int i = 100;
   public void Dispose()
   {
      Console.WriteLine("Dispose() called");
   }
   public void f()
   {
      Console.WriteLine("{0}", i); i  *= 2;
   }
}

public class Test
{
        public static void Main()
        {
                A a = new A();
                Console.WriteLine("Before using()");
                a.f();
                using ( a) 
                {
                    Console.WriteLine("Inside using()");
                    a.f();
                }
                Console.WriteLine("After using()");
                a.f();
        }
}

输出 (ideone):

Before using()
100
Inside using()
200
Dispose() called
After using()
400

如何在已释放的对象 a 上调用 f()?这样做是否允许?如果是,为什么?如果不是,那么为什么上面的程序在运行时没有抛出异常?


我知道使用 using 的流行结构是这样的:

using (A a = new A())
{
   //working with a
}

但我只是在试验,这就是为什么我写得不同的原因。


4
我看到有人忽略了C++中内存管理的确定性特性 :) - ChaosPandion
9
你的意思是:我编写了一个没有实现可处理对象协议的程序,运行时也没有实现可处理对象协议。而你需要负责实现这个行为。但你并没有这样做。现在去做吧。 - Eric Lippert
6个回答

20

"Disposed"并不意味着对象已经消失。 "Disposed" 只是表示释放了任何非托管资源(比如文件、连接等)。虽然这通常意味着对象不再提供任何有用的功能,但仍可能存在不依赖于非托管资源而仍能像平常一样工作的方法。

在 .NET(以及其衍生的 C#.net)中,Disposing 机制是存在的,因为它是一个垃圾回收环境,这意味着您无需负责内存管理。但是,垃圾回收器无法确定非托管资源是否已经停止使用,因此您需要自己完成此操作。

如果您希望在对象被释放后方法抛出异常,则需要使用布尔值来捕获“dispose”状态,并一旦对象被释放,就抛出异常:

public class A : IDisposable
{
   int i = 100;
   bool disposed = false;
   public void Dispose()
   {
      disposed = true;
      Console.WriteLine("Dispose() called");
   }
   public void f()
   {
      if(disposed)
        throw new ObjectDisposedException();

      Console.WriteLine("{0}", i); i  *= 2;
   }
}

Disposed 应该 意味着释放任何未托管的资源。但如果类实现不良,则不必如此。 - svick
显然,但这适用于任何关键行为,我假设了理想情况。 - Femaref
1
但是根据Dispose模式,当从客户端外部调用时,dispose意味着释放托管和非托管资源。在这种情况下,我预计没有任何方法可以工作。对吗? - OldSchool

9

异常未被抛出,是因为您没有设计方法在调用 Dispose 后抛出 ObjectDisposedException

CLR 不会自动知道一旦调用 Dispose 就应该抛出 ObjectDisposedException。如果 Dispose 已释放任何对于您的方法成功执行所必需的资源,则您有责任抛出异常。


5

一个典型的Dispose()实现只会调用存储在其字段中可释放的任何对象的Dispose()方法,从而释放非托管资源。如果您实现了IDisposable接口但实际上没有做任何事情(就像您在代码片段中所做的那样),则对象状态根本不会改变。什么也不会出错。不要混淆处理和终结。


4
IDisposable的目的是允许一个对象修复任何为其受益而被置于不理想状态的外部实体的状态。例如,Io.Ports.SerialPort对象可能会将串行端口的状态从“可供任何想要它的应用程序使用”更改为“只能由一个特定的Io.Ports.SerialPort对象使用”。SerialPort.Dispose的主要目的是将串行端口的状态恢复为“可供任何应用程序使用”。
当然,一旦实现IDisposable的对象重置了维护某个状态以使其受益的实体,它将不再拥有这些实体维护的状态的好处。例如,一旦串行端口的状态设置为“可供任何应用程序使用”,与之关联的数据流就不能再用于发送和接收数据。如果一个对象在没有将外部实体置于特殊状态的情况下可以正常工作,那么一开始就没有将外部实体置于特殊状态的理由。
通常,在对一个对象调用IDisposable.Dispose之后,不应该指望该对象能够做很多事情。试图在这样的对象上使用大多数方法将表明存在错误;如果一个方法不能合理地期望工作,正确的指示方式是通过ObjectDisposedException。
微软建议,几乎所有实现IDisposable的对象的大多数方法,如果在已释放的对象上使用它们,则应抛出ObjectDisposedException异常。我认为这样的建议过于宽泛。设备通常公开方法或属性以查找对象存活期间发生的事情是非常有用的。虽然可以给通信类一个Close方法和一个Dispose方法,只允许在关闭之后查询像NumberOfPacketsExchanged这样的东西,但这似乎过于复杂。读取与对象Disposed之前发生的事情有关的属性似乎是一种完全合理的模式。

3

调用Dispose()并不会将对象引用设置为null,而且您的自定义可处理类中也没有包含任何逻辑,如果在调用Dispose()之后访问其函数,则不会抛出异常,因此这是合法的。

在现实世界中,Dispose()释放非托管资源,这些资源以后将无法使用,并且/或者类作者让它在调用Dispose()后尝试使用对象时抛出ObjectDisposedException。通常,在Dispose()的主体内将设置一个类级别的布尔值,并在类的其他成员执行任何工作之前检查该值,如果bool为true,则抛出异常。


2

C#中的Dispose方法与C++中的析构函数不同。Dispose方法用于在对象仍然有效时释放托管(或非托管)资源。

根据类的实现,可能会抛出异常。如果f()不需要使用已处理的对象,则不一定需要抛出异常。


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