ChatGPT解决这个技术问题 Extra ChatGPT

您是否需要处理对象并将它们设置为空?

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

似乎有一个共识,您不需要将对象设置为 null,但是您需要执行 Dispose() 吗?
正如杰夫所说:codinghorror.com/blog/2009/01/…
如果一个对象实现了 IDisposable,我的建议总是处置。每次使用 using 块。不要做假设,不要把它留给机会。不过,您不必将内容设置为 null。一个对象刚刚超出范围。
@peter:不要对 WCF 客户端代理使用“使用”块:msdn.microsoft.com/en-us/library/aa355056.aspx
但是,您可能想在您的 Dispose() 方法中设置一些对 null 的引用!这是这个问题的一个微妙变化,但很重要,因为被处置的对象不知道它是否“超出范围”(不能保证调用 Dispose())。更多信息:stackoverflow.com/questions/6757048/…

p
participant

当不再使用对象并且垃圾收集器认为合适时,它们将被清理。有时,您可能需要将对象设置为 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();
    }
}

如果 obj 是引用类型,那么 finally 块等价于:if (obj != null) ((IDisposable)obj).Dispose();
@Tuzo:谢谢!编辑以反映这一点。
关于 IDisposable 的评论。在任何设计良好的类上,未能处置对象通常不会导致内存泄漏。在 C# 中使用非托管资源时,您应该有一个仍会释放非托管资源的终结器。这意味着不是在应该完成的时候释放资源,而是推迟到垃圾收集器最终确定托管对象时。但是,它仍然会导致许多其他问题(例如未释放的锁)。不过,您应该 Dispose 一个 IDisposable
@RandyLevy您对此有参考吗?谢谢
但我的问题是 Dispose() 需要实现任何逻辑吗?它必须做些什么吗?或者在内部当 Dispose() 被称为信号 GC 时好走?例如,我检查了 TextWriter 的源代码,而 Dispose 没有实现。
I
Igor Zevaka

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

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

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

在实践中,您不必担心破坏,它可以正常工作并且很棒:)

使用完 IDisposable 后,必须对所有实现 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 compared object references in Java 连接到气球上的字符串片段,气球是对象。同样的类比也适用于 C# 对象引用。它们只是指向包含对象的堆的位置。因此,将其设置为 null 不会立即影响对象的生命周期,气球会继续存在,直到 GC“弹出”它。

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

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

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

此外,.NET GC 在另一个线程(所谓的终结器线程)上运行,因为它有很多事情要做,并且在主线程上执行此操作会中断您的程序。


@Igor:“超出范围”是指对象引用脱离上下文,不能在当前范围内引用。当然,这仍然发生在 C# 中。
@Craig Johnston,不要将编译器使用的变量范围与由运行时确定的变量生命周期混淆——它们是不同的。局部变量可能不是“活动的”,即使它仍在范围内。
@Craig Johnston:请参阅 blogs.msdn.com/b/ericgu/archive/2004/07/23/192842.aspx:“如果不使用局部变量,则无法保证它会一直保持有效直到作用域结束。运行时可以自由分析它拥有的代码并确定有什么在某个点之后不再使用变量,因此不要让该变量在该点之后继续存在(即,出于 GC 的目的,不要将其视为根)。”
@Tuzo:是的。这就是 GC.KeepAlive 的用途。
@Craig Johnston:不,是的。不,因为 .NET 运行时会为您管理它并且做得很好。是的,因为程序员的工作不是编写(只是)编译的代码,而是编写运行的代码。有时它有助于了解运行时在幕后所做的事情(例如故障排除)。有人可能会争辩说,这是一种有助于区分优秀程序员和优秀程序员的知识。
G
Glorfindel

正如其他人所说,如果类实现 IDisposable,您肯定想调用 Dispose。我对此持相当严格的立场。例如,有些人可能会声称在 DataSet 上调用 Dispose 是没有意义的,因为他们反汇编它并发现它没有做任何有意义的事情。但是,我认为这种论点存在大量谬误。

阅读 this,了解受人尊敬的个人就该主题进行的有趣辩论。然后阅读我的推理here,为什么我认为 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 方法完成后说,那么您也错了。正确答案是它有资格在调用 DoSomething 期间的某个时间收集。没错。它是符合条件的引用设置为 null 之前,甚至可能在对 DoSomething 的调用完成之前。这是因为 JIT 编译器可以识别对象引用何时不再被取消引用,即使它们仍然是根目录。


如果 a 是类中的私有成员字段怎么办?如果 a 未设置为 null,则 GC 无法知道是否会在某些方法中再次使用 a,对吗?因此,在收集整个包含类之前,不会收集 a。不?
@凯文:正确。如果 a 是一个类成员,并且包含 a 的类仍然是 root 并且在使用中,那么它也会挂起。这是将其设置为 null 可能有益的一种情况。
您的观点与 Dispose 重要的原因有关——如果没有对它的根引用,就不可能在对象上调用 Dispose (或任何其他非内联方法);在使用对象完成之后调用 Dispose 将确保根引用将在对其执行的最后一个操作的整个持续时间内继续存在。具有讽刺意味的是,在不调用 Dispose 的情况下放弃对对象的所有引用可能会导致对象的资源偶尔过早释放。
这个例子和解释对于永远不要将引用设置为 null 的硬性建议似乎并不明确。我的意思是,除了 Kevin 的评论之外,在处理后设置为 null 的引用似乎非常良性,那么有什么害处呢?我错过了什么吗?
@dathompson 将其设置为 null 两次也将是“良性的”,但这不是这样做的理由。 (荒谬……)
E
EMP

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

是的,您应该处置实现 IDisposable 的对象。


如果您对大型对象有长期(甚至静态)引用,want 完成后立即将其清空,以便可以自由回收。
如果您曾经“完成”它不应该是静态的。如果它不是静态的,而是“长期存在的”,那么它应该在你完成它后很快就会超出范围。需要将引用设置为 null 表明代码结构存在问题。
您可以拥有一个已完成的静态项目。考虑:一个静态资源,它以用户友好的格式从磁盘读取,然后解析为适合程序使用的格式。您最终可能会得到一份原始数据的私人副本,但没有其他用途。 (现实世界的例子:解析是一个两遍例程,因此不能简单地处理读取的数据。)
如果只是临时使用,则不应将任何原始数据存储在静态字段中。当然,您可以这样做,正是由于这个原因,这不是一个好习惯:您必须手动管理其生命周期。
您可以通过将原始数据存储在处理它的方法中的局部变量中来避免它。该方法返回您保留的已处理数据,但是当方法退出并自动 GC 时,原始数据的本地超出范围。
C
Chris Schmich

如果对象实现 IDisposable,那么是的,您应该处置它。该对象可能会挂在本机资源(文件句柄、操作系统对象)上,否则可能不会立即释放。这可能导致资源匮乏、文件锁定问题和其他本可以避免的细微错误。

另请参阅 MSDN 上的 Implementing a Dispose Method


但是垃圾收集器不会调用 Dispose() 吗?如果是这样,你为什么需要调用它?
除非您明确调用它,否则无法保证会调用 Dispose。此外,如果您的对象持有稀缺资源或锁定某些资源(例如文件),那么您将希望尽快释放它。等待 GC 这样做是次优的。
GC 永远不会调用 Dispose()。 GC 可能会调用终结器,按照惯例应该清理资源。
@adrianm:不是 might 调用,而是 will 调用。
@leppie:终结器不是确定性的,可能不会被调用(例如,当 appdomain 被卸载时)。如果你需要确定性的终结,你必须实现我认为所谓的关键处理程序。 CLR 对这些对象进行了特殊处理,以确保它们被最终确定(例如,它预先处理终结代码以处理低内存)
J
John Saunders

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

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

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


A
Andre

如果它们实现了 IDisposable 接口,那么您应该处置它们。垃圾收集器将负责其余的工作。

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

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

p
peter

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

不要听乐比的。

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

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

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

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

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


G
GvS

当一个对象实现 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

M
Marnix van Valen

通常,不需要将字段设置为空。但是,我总是建议处置非托管资源。

根据经验,我还建议您执行以下操作:

如果您不再需要事件,请取消订阅它们。

如果不再需要任何包含委托或表达式的字段,请将其设置为 null。

我遇到了一些很难找到的问题,这些问题是不遵循上述建议的直接结果。

这样做的好地方是在 Dispose() 中,但通常越快越好。

一般来说,如果一个对象的引用存在,那么垃圾收集器 (GC) 可能需要几代的时间才能确定一个对象不再被使用。对象一直保留在内存中。

在您发现您的应用程序使用的内存比您预期的多得多之前,这可能不是问题。发生这种情况时,请连接内存分析器以查看哪些对象没有被清理。将引用其他对象的字段设置为 null 并在处理时清除集合可以真正帮助 GC 确定它可以从内存中删除哪些对象。 GC 将更快地回收已使用的内存,从而使您的应用程序的内存消耗更少,速度更快。


你对“事件和代表”是什么意思 - 应该用这些“清理”什么?
@Craig - 我编辑了我的答案。希望这能澄清一点。
H
Hui

我也必须回答。 JIT 通过对变量使用的静态分析生成表格和代码。这些表条目是当前堆栈帧中的“GC-Roots”。随着指令指针的前进,这些表条目变得无效,因此准备好进行垃圾回收。因此:如果它是一个作用域变量,则不需要将其设置为 null - GC 会收集该对象。如果它是成员或静态变量,则必须将其设置为 null


D
Dave

聚会有点晚了,但我认为这里没有提到一种情况 - 如果 A 类实现 IDisposable,并公开也是 IDisposable 对象的公共属性,那么我认为这不仅是 A 类的好习惯处理它在其 Dispose 方法中创建的一次性对象,但也将它们设置为 null。这样做的原因是,释放一个对象并让它被 GC(因为不再有对它的引用)绝不是一回事,尽管如果发生它肯定是一个错误。如果 A 类的客户端确实释放了其 ClassA 类型的对象,则该对象仍然存在。如果客户端随后尝试访问这些公共属性之一(现在也已被释放),结果可能会出乎意料。如果它们已被清空并被处置,则会立即出现空引用异常,这将使问题更易于诊断。