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

您是否需要处理对象并将其设置为null,或者当它们超出作用域时,垃圾收集器将清理它们?

317530 次浏览

如果它们实现了IDisposable接口,那么你应该释放它们。垃圾收集器会处理剩下的事情。

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

using(var con = new SqlConnection("..")){ ...

在c#中,你永远不需要将对象设置为空。编译器和运行时将负责找出它们何时不再在作用域中。

是的,你应该处理实现IDisposable的对象。

如果对象实现了IDisposable,那么是的,你应该释放它。对象可能挂在本地资源(文件句柄、操作系统对象)上,否则这些资源可能不会立即释放。这可能导致资源短缺、文件锁定问题和其他本来可以避免的微妙错误。

请参见MSDN上的实现Dispose方法

当对象不再被使用并且垃圾回收器认为合适时,对象将被清理。有时,你可能需要将一个对象设置为null以使其超出作用域(例如你不再需要其值的静态字段),但总的来说,通常不需要将其设置为null

关于处置对象,我同意@Andre的观点。如果对象是IDisposable,当你不再需要它时,它就是处理掉它是个好主意,特别是当对象使用非托管资源时。不处理非托管资源将导致内存泄漏

一旦程序离开using语句的作用域,就可以使用using语句自动处理对象。

using (MyIDisposableObject obj = new MyIDisposableObject())
{
// use the object here
} // the object is disposed here

其功能等价于:

MyIDisposableObject obj;
try
{
obj = new MyIDisposableObject();
}
finally
{
if (obj != null)
{
((IDisposable)obj).Dispose();
}
}

在c#中,对象永远不会像在c++中那样超出范围。当它们不再被使用时,垃圾回收器会自动处理它们。这是一种比c++更复杂的方法,在c++中,变量的作用域是完全确定的。CLR垃圾收集器会主动遍历所有已创建的对象,并判断它们是否正在被使用。

一个对象可以在一个函数中“超出作用域”,但如果它的值被返回,那么GC将查看调用函数是否持有返回值。

将对象引用设置为null是不必要的,因为垃圾收集是通过确定哪些对象正在被其他对象引用来工作的。

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

当你完成对所有实现了IDisposable的对象的操作时,必须对它们调用Dispose。通常你会像这样使用using块来处理这些对象:

using (var ms = new MemoryStream()) {
//...
}

编辑变量作用域。Craig问过变量作用域是否对对象的生存期有任何影响。为了正确地解释CLR的这一方面,我需要解释c++和c#中的一些概念。

实际变量范围

在这两种语言中,变量只能在定义的范围内使用——类、函数或用花括号括起来的语句块。然而,微妙的区别是,在c#中,变量不能在嵌套块中重新定义。

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

int iVal = 8;
//iVal == 8
if (iVal == 8){
int iVal = 5;
//iVal == 5
}
//iVal == 8

在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
}

如果查看生成的MSIL,这是有意义的——函数使用的所有变量都在函数开始时定义。看看这个函数:

public static void Scope() {
int iVal = 8;
if(iVal == 8) {
int iVal2 = 5;
}
}

下面是生成的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

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

在堆上创建c++对象时,必须显式地销毁它们,否则就会造成内存泄漏。但是堆栈变量没有这样的问题。

c#对象生存期

在CLR中,对象(即引用类型)是在托管堆上创建的总是。对象创建语法进一步加强了这一点。考虑下面的代码片段。

MyClass stackObj;

在c++中,这将在堆栈上的MyClass上创建一个实例,并调用其默认构造函数。在c#中,它将创建一个对类MyClass的引用,该引用不指向任何东西。创建类实例的唯一方法是使用new操作符:

MyClass stackObj = new MyClass();

在某种程度上,c#对象很像c++中使用new语法创建的对象——它们是在堆上创建的,但与c++对象不同的是,它们由运行时管理,所以你不必担心销毁它们。

由于对象是堆上的总是,对象引用(即指针)超出作用域的事实变得毫无意义。在确定是否收集对象时,涉及到的因素不仅仅是对对象的引用。

c#对象引用

Jon Skeet 比较Java中的对象引用到连接到气球上的绳子,这是对象。同样的类比也适用于c#对象引用。它们只是指向包含该对象的堆的位置。因此,将它设置为null对对象的生命周期没有立即影响,气球继续存在,直到GC“弹出”它。

继续气球的类比,似乎合乎逻辑的是,一旦气球上没有了绳子,它就可以被摧毁。事实上,这正是引用计数对象在非托管语言中的工作方式。但是这种方法对于循环引用不太适用。想象一下,两个气球用一根绳子连在一起,但两个气球都没有与其他任何东西相连的绳子。在简单的裁判计数规则下,它们都继续存在,即使整个气球组是“孤儿”。

. net对象很像屋顶下的氦气球。当屋顶打开(GC运行)-未使用的气球飘走,即使有可能有一组气球拴在一起。

. net GC使用分代GC和标记和清除的组合。分代方法涉及到运行时倾向于检查最近分配的对象,因为它们更有可能未使用,而标记和扫描涉及到运行时遍历整个对象图,并确定是否有未使用的对象组。这充分地解决了循环依赖问题。

此外,. net GC运行在另一个线程(所谓的终结器线程)上,因为它有相当多的事情要做,在主线程上这样做会中断你的程序。

通常,不需要将字段设置为null。然而,我总是建议丢弃非托管资源。

根据我的经验,我还建议你这样做:

  • 如果不再需要,则取消订阅事件。
  • 如果不再需要,将持有委托或表达式的任何字段设置为空。

我遇到过一些很难发现的问题,这些问题都是由于没有遵循上述建议而直接导致的。

Dispose()是执行此操作的一个好地方,但通常越快越好。

一般来说,如果存在对某个对象的引用,垃圾收集器(GC)可能要多花几代时间才能确定该对象不再使用。在此期间,对象始终保留在内存中。

这可能不是一个问题,直到你发现你的应用程序正在使用比你预期的更多的内存。当发生这种情况时,连接内存分析器来查看哪些对象没有被清理。将引用其他对象的字段设置为null并在处理时清除集合可以真正帮助GC确定可以从内存中删除哪些对象。GC将更快地回收使用的内存,使您的应用程序的内存需求更少,更快。

正如其他人所说,如果类实现了IDisposable,则肯定要调用Dispose。我在这个问题上的立场相当坚定。例如,有些人可能会声称在DataSet上调用Dispose是没有意义的,因为他们分解了它,看到它没有做任何有意义的事情。但是,我认为这种说法有很多谬误。

阅读,了解受人尊敬的人就这个问题进行的有趣辩论。然后阅读我的推理,为什么我认为Jeffery Richter是在错误的阵营。

现在,关于是否应该设置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");
}

那么你认为a引用的对象什么时候适合被收集呢?如果你说在调用a = null之后,那么你就错了。如果你说在Main方法完成之后,那么你也错了。正确的答案是它有资格在某个时候被收集。没错。它是符合条件的之前,引用被设置为null,甚至可能在DoSomething调用完成之前。这是因为JIT编译器可以识别对象引用何时不再被解引用,即使它们仍然是根引用。

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

别听她的。

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

可能发生的一件事是,很多物体都可以保持打开状态。这将极大地增加应用程序的内存使用。有时很难判断这究竟是内存泄漏,还是您的应用程序只是在做很多事情。

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

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

在后台,代码写得很糟糕,但关键是你要确保没有使用的东西超出了范围。这需要很长时间才能用内存分析器找到,因为很难知道内存中的哪些东西是有效的,哪些不应该在那里。

我同意这里常见的答案,是的,你应该处理,不,你通常不应该设置变量为null…但是我想指出dispose主要不是关于内存管理的。是的,它可以帮助内存管理(有时确实如此),但它的主要目的是让您确定地释放稀缺资源。

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

我在现实世界中见过这样做导致问题的例子。例如,我看到ASP。Net web应用程序最终无法连接到数据库(虽然是很短的一段时间,或者直到web服务器进程重新启动),因为sql server '连接池已满'…也就是说,在如此短的时间内创建了如此多的连接,并且没有显式地释放,以至于没有新的连接可以创建,池中的许多连接虽然不是活动的,但仍然被未分配和未收集的对象引用,因此不能重用。在必要的地方正确处理数据库连接可以确保这个问题不会发生(至少不会,除非你有非常高并发访问)。

当一个对象实现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
我也要回答。 JIT从对变量使用情况的静态分析中生成表和代码。 这些表项是当前堆栈帧中的“GC-Roots”。随着指令指针的前进,这些表项变得无效,因此准备进行垃圾收集。 因此:如果它是一个作用域变量,你不需要将它设置为null - GC将收集该对象。 如果它是一个成员或静态变量,则必须将其设置为null

有点晚了,但是有一个场景我认为这里没有提到——如果类A实现了IDisposable,并且暴露了同样是IDisposable对象的公共属性,那么我认为对类A来说,不仅要处理它在dispose方法中创建的可丢弃对象,而且还要将它们设置为null。这样做的原因是销毁一个对象和让它得到GCed(因为没有对它的更多引用)绝不是一回事,尽管如果发生这种情况,它肯定是一个错误。如果Class a的客户端释放了其ClassA类型的对象,则该对象仍然存在。如果客户端随后试图访问这些公共属性之一(现在也已被释放),结果可能非常出乎意料。如果它们既被置空又被处理,将立即出现空引用异常,这将使问题更容易诊断。