你需要处理对象并将它们设置为null吗?

CJ7*_*CJ7 300 .net c# garbage-collection dispose

您是否需要处理对象并将它们设置为null,或者当垃圾收集器超出范围时它们是否会将它们清理干净?

Zac*_*son 236

当物体不再被使用以及垃圾收集器看起来合适时,物体将被清理干净.有时,您可能需要将对象设置null为使其超出范围(例如,您不再需要其值的静态字段),但总体而言通常无需设置null.

关于处理对象,我同意@Andre.如果对象是在您不再需要它时处置IDisposable它是一个好主意,特别是如果对象使用非托管资源.不处理非托管资源会导致内存泄漏.

using一旦程序离开using语句的范围,您可以使用该语句自动处理对象.

using (MyIDisposableObject obj = new MyIDisposableObject())
{
    // use the object here
} // the object is disposed here
Run Code Online (Sandbox Code Playgroud)

这在功能上等同于:

MyIDisposableObject obj;
try
{
    obj = new MyIDisposableObject();
}
finally
{
    if (obj != null)
    {
        ((IDisposable)obj).Dispose();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 如果obj是引用类型,则finally块等效于:`if(obj!= null)((IDisposable)obj).Dispose();` (4认同)
  • 关于"IDisposable"的一句话.在任何精心设计的类中,未能处理对象通常不会导致内存泄漏**.在C#中使用非托管资源时,您应该有一个仍将释放非托管资源的终结器.这意味着,当应该完成资源时,不会取消分配资源,而是在垃圾收集器完成托管对象时将其推迟.它仍然可能导致许多其他问题(例如未发布的锁).你应该配置一个`IDisposable`! (2认同)

Igo*_*aka 133

对象永远不会像在C++中那样超出C#的范围.它们在不再使用时由垃圾收集器自动处理.这是一种比C++更复杂的方法,其中变量的范围完全是确定性的.CLR垃圾收集器主动遍历已创建的所有对象,如果正在使用它们则可以解决.

对象可以在一个函数中"超出范围",但如果返回其值,则GC将查看调用函数是否保留在返回值上.

设置对象引用null是不必要的,因为垃圾收集通过计算其他对象引用哪些对象来工作.

在实践中,你不必担心破坏,它只是工作,它是伟大的:)

Dispose必须在IDisposable完成使用它们时实现的所有对象上调用.通常你会使用using这些对象的块,如下所示:

using (var ms = new MemoryStream()) {
  //...
}
Run Code Online (Sandbox Code Playgroud)

编辑可变范围.Craig已经询问变量范围是否对对象生命周期有任何影响.为了正确解释CLR的这个方面,我需要从C++和C#中解释一些概念.

实际变量范围

在这两种语言中,变量只能在与定义相同的范围内使用 - 类,函数或括号括起来的语句块.然而,细微差别在于,在C#中,无法在嵌套块中重新定义变量.

在C++中,这是完全合法的:

int iVal = 8;
//iVal == 8
if (iVal == 8){
    int iVal = 5;
    //iVal == 5
}
//iVal == 8
Run Code Online (Sandbox Code Playgroud)

但是在C#中,您会遇到编译器错误:

int iVal = 8;
if(iVal == 8) {
    int iVal = 5; //error CS0136: A local variable named 'iVal' cannot be declared in this scope because it would give a different meaning to 'iVal', which is already used in a 'parent or current' scope to denote something else
}
Run Code Online (Sandbox Code Playgroud)

如果查看生成的MSIL,这是有意义的 - 函数使用的所有变量都是在函数的开头定义的.看看这个功能:

public static void Scope() {
    int iVal = 8;
    if(iVal == 8) {
        int iVal2 = 5;
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是生成的IL.请注意,在if块中定义的iVal2实际上是在功能级别定义的.实际上,这意味着就可变生命周期而言,C#仅具有类和功能级别范围.

.method public hidebysig static void  Scope() cil managed
{
  // Code size       19 (0x13)
  .maxstack  2
  .locals init ([0] int32 iVal,
           [1] int32 iVal2,
           [2] bool CS$4$0000)

//Function IL - omitted
} // end of method Test2::Scope
Run Code Online (Sandbox Code Playgroud)

C++范围和对象生存期

每当在堆栈上分配的C++变量超出范围时,它就会被破坏.请记住,在C++中,您可以在堆栈或堆上创建对象.当您在堆栈上创建它们时,一旦执行离开作用域,它们就会从堆栈中弹出并被销毁.

if (true) {
  MyClass stackObj; //created on the stack
  MyClass heapObj = new MyClass(); //created on the heap
  obj.doSomething();
} //<-- stackObj is destroyed
//heapObj still lives
Run Code Online (Sandbox Code Playgroud)

在堆上创建C++对象时,必须显式销毁它们,否则会导致内存泄漏.虽然堆栈变量没有这样的问题.

C#对象生命周期

在CLR中,始终在托管堆上创建对象(即引用类型).对象创建语法进一步强化了这一点.请考虑此代码段.

MyClass stackObj;
Run Code Online (Sandbox Code Playgroud)

在C++中,这将MyClass在堆栈上创建一个实例并调用其默认构造函数.在C#中,它将创建对MyClass不指向任何内容的类的引用.创建类实例的唯一方法是使用new运算符:

MyClass stackObj = new MyClass();
Run Code Online (Sandbox Code Playgroud)

在某种程度上,C#对象很像使用newC++中的语法创建的对象- 它们是在堆上创建的,但与C++对象不同,它们由运行时管理,因此您不必担心破坏它们.

由于对象总是在堆上,因此对象引用(即指针)超出范围的事实变得没有实际意义.确定是否要收集对象涉及的因素多于简单地存在对象的引用.

C#对象引用

Jon Skeet 将Java中的对象引用与附加到气球(即对象)的字符串进行了比较.相同的类比适用于C#对象引用.它们只是指向包含该对象的堆的位置.因此,将其设置为null对对象生命周期没有立即影响,气球继续存在,直到GC"弹出"它.

继续按照气球的比喻,似乎合乎逻辑的是,一旦气球没有任何附加条件,它就会被摧毁.实际上,这正是引用计数对象在非托管语言中的工作方式.除非这种方法不适用于循环引用.想象一下两个气球通过一个字符串连接在一起,但气球都没有任何其他字符串.在简单的引用计数规则下,它们都会继续存在,即使整个气球组是"孤立的".

.NET对象很像屋顶下的氦气球.当屋顶打开(GC运行)时 - 未使用的气球会漂浮,即使可能有一组气球系在一起.

.NET GC使用世代GC和标记和扫描的组合.分代方法涉及运行时有利于检查最近分配的对象,因为它们更可能未被使用,并且标记和扫描涉及运行时通过整个对象图并且如果存在未使用的对象组则进行计算.这充分解决了循环依赖问题.

此外,.NET GC在另一个线程(所谓的终结器线程)上运行,因为它有很多工作要做,在主线程上这样做会中断你的程序.


Bri*_*eon 17

正如其他人所说,Dispose如果该类实现,你肯定想要调用IDisposable.我对此采取了相当严格的立场.有些人可能会声称,呼吁DisposeDataSet,例如,是毫无意义的,因为他们拆开它,看到它没有做任何有意义.但是,我认为这个论点中存在很多谬误.

请阅读此内容,以便受尊敬的人就此主题进行有趣的辩论.然后在这里阅读我的推理,为什么我认为杰弗里里希特在错误的阵营.

现在,关于你是否应该设置引用null.答案是不.让我用以下代码说明我的观点.

public static void Main()
{
  Object a = new Object();
  Console.WriteLine("object created");
  DoSomething(a);
  Console.WriteLine("object used");
  a = null;
  Console.WriteLine("reference set to null");
}
Run Code Online (Sandbox Code Playgroud)

那么你认为引用的对象什么时候a有资格收集?如果你说完电话a = null后你就错了.如果您在Main方法完成后说,那么您也错了.正确的答案是它有资格通话期间收集DoSomething.那是对的.在引用设置之前,甚至在完成调用之前,它是合格的.这是因为JIT编译器可以识别对象引用何时不再解除引用,即使它们仍然是root的.nullDoSomething

  • @Kevin:正确.如果`a`是一个类成员,并且包含`a`的类仍然是root并且正在使用中,那么它也会挂起.这是将其设置为"null"的一种情况可能是有益的. (4认同)
  • 如果`a`是一个类中的私有成员字段怎么办?如果`a`没有设置为null,GC无法知道某个方法是否会再次使用`a`,对吧?因此,在收集整个包含类之前,不会收集"a".没有? (3认同)

EMP*_*EMP 13

您永远不需要在C#中将对象设置为null.编译器和运行时将负责确定它们何时不在范围内.

是的,您应该处理实现IDisposable的对象.

  • 如果你曾"完成它"它不应该是静态的.如果它不是静态的,而是"长寿命",那么在你完成之后它仍然应该很快就会超出范围.将引用设置为null的需要表明代码结构存在问题. (12认同)
  • 如果你对一个大对象有一个长期(甚至是静态的)引用,那么你想要在它完成之后立即将它清空,以便可以自由地回收它. (2认同)
  • 如果只是临时使用,则不应将任何原始数据存储在静态字段中。当然,你*可以*这样做,但这并不是一个好的做法,正是因为这个原因:然后你必须手动管理它的生命周期。 (2认同)
  • 您可以通过将原始数据存储在处理它的方法中的局部变量中来避免它.该方法返回您保留的已处理数据,但当方法退出并自动GC时,原始数据的本地超出范围. (2认同)

Chr*_*ich 11

如果对象实现IDisposable,那么是的,你应该处理它.该对象可能会挂起到本机资源(文件句柄,OS对象),否则可能无法立即释放.这可能导致资源匮乏,文件锁定问题以及其他可能无法避免的细微错误.

另请参阅在MSDN上实现Dispose方法.

  • GC永远不会调用Dispose().GC可能会调用终结器,按照惯例,它应该清理资源. (12认同)
  • @leppie:终结器不是确定性的,可能不会被调用(例如,卸载appdomain时).如果您需要确定性的最终确定,您必须实现我认为称为关键处理程序的东西.CLR对这些对象进行了特殊处理,以保证它们最终确定(例如,它会预先确定最终化代码以处理低内存) (2认同)

Yor*_*ort 11

我同意这里的常见答案是肯定你应该处理,并且通常不应该将变量设置为null ...但我想指出dispose主要不是关于内存管理.是的,它可以帮助(有时确实)内存管理,但它的主要目的是为您提供确定性的稀缺资源发布.

例如,如果您打开硬件端口(例如串行),TCP/IP套接字,文件(独占访问模式)甚至是数据库连接,您现在已经阻止任何其他代码使用这些代码,直到它们被释放.处理通常会释放这些物品(以及GDI和其他"os"手柄等,其中有1000种可用,但总体上仍然有限).如果您没有在所有者对象上调用dipose并显式释放这些资源,那么尝试将来再次打开相同的资源(或另一个程序),打开尝试将失败,因为未处理的未收集对象仍然打开该项目.当然,当GC收集项目时(如果Dispose模式已经正确实现),资源将被释放......但是你不知道什么时候会这样,所以你不要 知道何时重新打开该资源是安全的.这是Dispose解决的主要问题.当然,释放这些句柄通常也会释放内存,从不释放它们可能永远不会释放内存......因此所有关于内存泄漏或内存清理延迟的讨论.

我已经看到了这个导致问题的现实世界的例子.例如,我看到ASP.Net Web应用程序最终无法连接到数据库(尽管很短的时间,或者直到Web服务器进程重新启动),因为sql server的"连接池已满"...即,如此多的连接已经创建,并且在如此短的时间内没有明确释放,因此无法创建新的连接,并且池中的许多连接虽然不活动,但仍然被未填充和未收集的对象引用,因此可以'重复使用.在必要时正确配置数据库连接可确保不会发生此问题(至少除非您具有非常高的并发访问权限).


And*_*dre 9

如果他们实现IDisposable接口,那么你应该处理它们.垃圾收集器将处理其余的事情.

编辑:最好是using在处理一次性物品时使用该命令:

using(var con = new SqlConnection("..")){ ...
Run Code Online (Sandbox Code Playgroud)


pet*_*ter 5

总是调用 dispose。不值得冒这个险。应尊重大型托管企业应用程序。不能做任何假设,否则它会回来咬你。

不要听leppie的。

很多对象实际上并没有实现 IDisposable,因此您不必担心它们。如果它们真的超出范围,它们将被自动释放。此外,我从未遇到过必须将某些内容设置为空的情况。

可能发生的一件事是许多对象可以保持打开状态。这会大大增加应用程序的内存使用量。有时很难确定这是否真的是内存泄漏,或者您的应用程序是否只是在做很多事情。

内存配置文件工具可以帮助解决此类问题,但可能会很棘手。

此外,始终取消订阅不需要的事件。还要小心 WPF 绑定和控件。不是通常的情况,但我遇到了一种情况,我有一个绑定到底层对象的 WPF 控件。底层对象很大,占用了大量内存。WPF 控件正在被一个新实例替换,而旧的控件由于某种原因仍然存在。这导致了大量内存泄漏。

在 hindsite 中,代码写得不好,但关键是您要确保未使用的东西超出范围。使用内存分析器需要很长时间才能找到那个,因为很难知道内存中的哪些内容是有效的,哪些内容不应该存在。


GvS*_*GvS 5

当一个对象实现时,IDisposable您应该调用Dispose(或者Close,在某些情况下,它将为您调用Dispose)。

通常,您不必将对象设置为null,因为GC将知道不再使用某个对象。

将对象设置为时,有一个例外null。当我从数据库中检索许多需要处理的对象并将它们存储在集合(或数组)中时。完成“工作”后,我将对象设置为null,因为GC不知道我已经完成了工作。

例:

using (var db = GetDatabase()) {
    // Retrieves array of keys
    var keys = db.GetRecords(mySelection); 

    for(int i = 0; i < keys.Length; i++) {
       var record = db.GetRecord(keys[i]);
       record.DoWork();
       keys[i] = null; // GC can dispose of key now
       // The record had gone out of scope automatically, 
       // and does not need any special treatment
    }
} // end using => db.Dispose is called
Run Code Online (Sandbox Code Playgroud)