【问题标题】:C# Clearing list before nullingC# 在清空之前清除列表
【发布时间】:2018-11-03 02:51:35
【问题描述】:

今天我看到了一段乍看之下很奇怪的代码,让我重新考虑。这是代码的简化版本:

if(list != null){
    list.Clear();
    list = null;
}

我的想法是,为什么不简单地替换它:

list = null;

我读了一点,我知道清除列表将删除对允许 GC 执行此操作但不会“调整大小”的对象的引用。为该列表分配的内存保持不变。

另一方面,设置为 null 也会删除对列表的引用(以及对它的项),也允许 GC 做这件事。

所以我一直在试图找出一个像第一个街区那样做的理由。我想到的一种情况是,如果您有两个对列表的引用。第一个块将清除列表中的项目,因此即使第二个引用仍然存在,GC 仍然可以清除为这些项目分配的内存。

不过,我觉得这有点奇怪,所以我想知道我提到的场景是否有意义?

另外,是否还有其他情况我们必须在将引用设置为 null 之前 Clear() 一个列表?

最后,如果我提到的场景是合理的,那么确保我们不会同时持有对该列表的多个引用以及我们将如何(明确地)这样做不是更好吗?

编辑:我明白清空列表和清空列表之间的区别。我很想知道 GC 内部是否有什么东西可以做到这一点,以便有理由在 Nulling 之前清除。

【问题讨论】:

  • 作为一般规则,将其设置为null 是您通常应该做的。一旦 GC 意识到 List 不可访问 - 它将意识到其内容(即底层数组及其指向的内容)也是不可访问的。在某些情况下,清除它可能是值得的——例如,如果有多个对底层 List 的引用,并且您不只是想摆脱对 List 的引用,但您想确保所有other 引用指向一个空列表(正如您在问题中确定的那样)。公平地说,这种情况很少见。
  • wouldn't it be better off to just make sure we don't hold multiple references to this list at once and how would we do that (explicitly) 除了通过作用域(即,将列表作为函数作用域或private,并且从不将其暴露在外部)之外,没有简单的方法可以做到这一点。
  • @mjwills 在这种情况下,它是私有的,只在类内部使用。
  • 在这种情况下,它们是等价的。
  • 在清空保存列表的变量之前立即调用List.Clear() 是没有用的如果这是对该列表的唯一引用。假设是这种情况,要么作者不知道这一点,要么他可能都混淆了List.Clear 与其他Clear 方法,这些方法是Dispose 的同义词(在他们的类中)(例如,Stream.Dispose)和b) 因此认为List.Clear 是一种“深层Dispose”,可以单独处理列表中的每个元素。

标签: c# .net icollection


【解决方案1】:

list.Clear() 在您的场景中不是必需的(其中List 是private 并且仅在类中使用)。

关于可达性/活动对象的一个​​很好的介绍级链接是http://levibotelho.com/development/how-does-the-garbage-collector-work:

垃圾收集器如何识别垃圾?

在微软的 垃圾收集器确定的 .NET 框架的实现 如果一个对象是垃圾通过检查引用类型变量 指向它。在垃圾收集器的上下文中,参考 类型变量被称为“根”。根的例子包括:

  • 堆栈上的引用
  • 静态变量中的引用
  • 托管堆上另一个对象中不符合垃圾条件的引用 收藏
  • 方法中局部变量形式的引用

此上下文中的关键位是托管堆上不符合垃圾回收条件的另一个对象中的引用。因此,如果List 有资格被收集(并且列表中的对象未在其他地方引用),那么列表中的那些对象也有资格被收集。

换句话说,GC 将意识到list 及其内容在在同一通道中是不可访问的。

那么,有没有list.Clear() 有用的实例?是的。如果您有两个对单个 List 的引用(例如,作为两个不同对象中的两个字段),这可能会很有用。其中一个引用可能希望以另一种引用也受到影响的方式清除列表 - 其中list.Clear() 是完美的。

【讨论】:

    【解决方案2】:

    这个答案最初是对Mick 的评论,他声称:

    这取决于您使用的 .NET 版本。在 Xamarin 或 mono 等移动平台上,您可能会发现垃圾收集器需要这种帮助才能完成工作。

    那句话是在乞求事实核查。那么,让我们看看...


    .NET

    .NET 使用分代标记和清除垃圾收集器。您可以在What happens during a garbage collection 中看到算法的摘要。总而言之,它会遍历对象图,如果无法到达某个对象,则可以擦除该对象。

    因此,垃圾收集器将在同一迭代中正确地将列表中的项目识别为可回收,无论您是否清除列表。无需事先解耦对象。

    这意味着清除列表不会帮助垃圾收集器对 .NET 的常规实现。

    注意:如果有另一个对列表的引用,那么您清除列表的事实将是可见的。


    Mono 和 Xamarin

    单声道

    事实证明,Mono 也是如此。

    Xamarin.Android

    Xamarin.Android 也是如此。

    Xamarin.iOS

    但是,Xamarin.iOS 需要其他注意事项。特别是,MonoTouch 将使用超出垃圾收集器的wrapped Objective-C objects。见Avoid strong circular references under iOS Performance。这些对象需要不同的语义。

    Xamarin.iOS 将通过保留缓存来最小化 Objetive-C 对象的使用:

    当您调用返回 NSObject 的方法或属性时,也会按需创建 C# NSObject。此时,运行时将查看对象缓存并确定给定的 Objective-C NSObject 是否已经出现在托管世界中。如果对象已浮出水面,则返回现有对象,否则将调用以 IntPtr 作为参数的构造函数来构造对象。

    即使没有来自托管代码的引用,系统也会使这些对象保持活动状态:

    NSObject 的用户子类通常包含 C# 状态,因此每当 Objective-C 运行时对这些对象之一执行“retain”操作时,运行时会创建一个保持托管状态的 GCHandle对象是活动的,即使没有对该对象的 C# 可见引用。这大大简化了簿记,因为状态会自动为您保留。

    强调我的。

    因此,在 Xamarin.iOS 下,如果列表可能包含包装的 Objetive-C 对象,则此代码将有助于垃圾收集器。

    请参阅问题How does memory management works on Xamarin.IOS、Miguel de Icaza explains in his answer,语义是在获取引用时“保留”对象,在引用时“释放”对象为空。

    在 Objetive-C 方面,“释放”并不意味着销毁对象。 Objetive-C 使用引用计数垃圾收集器。当我们“retain”对象时,计数器会增加,而当我们“release”时,计数器会减少。当计数器达到零时,系统会销毁对象。请参阅:About Memory Management。

    因此,Objetive-C 不擅长处理循环引用(如果 A 引用 B 和 B 引用 A,它们的引用计数不为零,即使它们无法到达),因此,你应该在 Xamarin.iOS 中避免使用它们。事实上,忘记解耦引用会导致 Xamarin.iOS 中的泄漏...请参阅:Xamarin iOS memory leaks everywhere。


    其他

    dotGNU 还使用分代标记和清除垃圾收集器。

    我还查看了 CrossNet(将 IL 编译为 C++),看来他们也尝试实现它。不知道好不好。

    【讨论】:

    • 很好的分析。我并不打算建议 CLI 的任何特定实现更容易受到内存泄漏的影响,这就是我经常使用“可能”这个词的原因。只是垃圾收集器的健壮性不能保证从一种实现到另一种实现。当然,在这些平台上从一个版本到另一个版本又是另一回事了。 Xamarin 和 Mono 只是可能更容易受到内存泄漏影响的平台示例。我敢打赌,Raspberry Pi 上的 .NET 可能是另一个候选者。
    • @Mick .NET Compact Framework (ARM) 和 .NET Micro Framework (Netduino) 都使用标记和扫描。它们实际上更容易,因为它们不允许您处理指针,因此不需要固定对象。但是性能是个问题。我不知道 Raspberry 是否曾经使用其中之一运行过,但是,它可以运行 Mono 和 .NET Core,它们是标记和扫描的。如果您忘记处置本机包装器,则可能会发生泄漏,而不管 GC。至于版本,所有这些 GC 都基于 Boehm GC,其原始版本早于 .NET,大多数平台已转向并发变体。
    • 是的,我并没有声称对 GC 的任何实现有详细的了解。不过我有他们的经验。自从我使用 .NET Compact Framework 已经有一段时间了,十年前我确实遇到了在 .NET Compact Framework 上运行的应用程序的内存问题,这肯定通过解耦解决了。我想从那时起,紧凑的框架 GC 已经有了重大的升级。运行紧凑型框架的设备的处理能力肯定会猛增,这将使 GC 在 CPU 使用方面更具侵略性。
    • @Mick 十年前是大多数人使用那个东西的时候 - 为它构建它是一种痛苦,但在 Visual Studio 2008 之外的任何其他东西中构建它 - 据我所知 - 不可能.我当然不会安装它来检查它有多糟糕。如果我没记错的话,GC 很慢,开始运行可能需要很长时间,并且垃圾会蔓延。当它运行时,它会停止你的程序。我认为GC.Collect 在预定义位置上是常见的做法,既可以减少垃圾,又可以控制暂停。不...让我们假装没有人使用它,然后忘记它。
    【解决方案3】:

    这取决于您使用的 .NET 版本。在 Xamarin 或 mono 等移动平台上,您可能会发现垃圾收集器需要这种帮助才能完成工作。而在桌面平台上,垃圾收集器的实现可能更加复杂。 CLI 的每个实现都将拥有自己的垃圾收集器实现,并且它的行为可能因一种实现而异。

    我记得 10 年前开发的 Windows Mobile 应用程序存在内存问题,而这种代码就是解决方案。这可能是由于移动平台需要一个垃圾收集器,它的处理能力比桌面更节俭。

    解耦对象有助于简化垃圾收集器需要执行的分析,并有助于避免垃圾收集器无法识别大型对象图实际上已与应用程序中的所有线程断开连接的情况。这会导致内存泄漏。

    任何认为在 .NET 中不会发生内存泄漏的人都是缺乏经验的 .NET 开发人员。在桌面平台上,确保在实现它们的对象上调用 Dispose 可能就足够了,但是对于其他实现,您可能会发现它不是。

    List.Clear() 将列表中的对象与列表以及彼此分离。

    编辑:所以要明确的是,我并不是说目前存在的任何特定实现都容易受到内存泄漏的影响。再次取决于何时读取此答案,垃圾收集器在当前 CLI 的任何实现上的鲁棒性可能会在编写本文时发生变化。

    基本上我的建议是,如果您知道您的代码需要跨平台并在 .NET 框架的许多实现中使用,尤其是用于移动设备的 .NET 框架的实现,那么花时间来解耦对象可能是值得的当不再需要它们时。在这种情况下,我会首先向已经实现 Dispose 的类添加解耦,然后如果需要,请查看在未实现 IDisposable 的类上实现 IDisposable 并确保在这些类上调用 Dispose。

    如何确定是否需要它?您需要在要部署的每个平台上检测和监控应用程序的内存使用情况。与其编写大量多余的代码,我认为最好的方法是等到您的监控工具显示您有内存泄漏。

    【讨论】:

    • List.Clear() 将不会将列表中的对象彼此分离。这不是某种“深刻的清晰”。它只会将元素与列表结构本身分离。
    • @davidbak 虽然在列表中,就垃圾收集器而言,它们在同一个对象图中,因此通过列表间接耦合在一起,这就是我最初声明的含义。如果列表的成员相互引用,则为真,List.Clear() 不会解耦这些引用。
    【解决方案4】:

    如docs中提到的:

    List.Clear Method():Count设置为0,并引用其他 集合元素中的对象也会被释放。

    在您的第一个 sn-p 中:

    if(list != null){
        list.Clear();
        list = null;
    }
    

    如果你只是将list设置为null,这意味着你将list的引用释放到内存中的实际对象(所以list本身仍然保留在内存中)并等待因为垃圾收集器来并释放其分配的内存。

    但是问题是您的列表可能包含包含对另一个对象的引用的元素,例如:

    list → objectA, objectB, objectC
    objectB → objectB1, objectB2
    

    所以,在将list 设置为null 后,现在列表没有引用,应该稍后由垃圾收集器收集,但objectB1 和objectB2 有来自的引用objectB(仍然在内存中)因此,垃圾收集器需要分析对象引用链。为了减少混淆,这个 sn-p 使用.Clear() 函数来消除这种混淆。

    【讨论】:

    • 所以如果我理解正确,这样做是为了防止在对象持有对象引用的情况下进行缓慢的垃圾收集(并且它可能会继续 objectB1 -> objectB3 -> objectB4),因为垃圾收集器会通过收集的迭代和每次迭代都必须重新评估不再引用的项目(而不是说,在一个循环中收集所有对象及其对其他对象的引用)?
    • 您似乎是在暗示,如果ObjectA 持有对ObjectB 的引用并且ObjectB 有对ObjectC 的引用,那么GC 的一次传递将意识到ObjectA 是无法到达并清理它。然后下一遍会意识到ObjectB 不可访问并清理它。然后下一次将意识到ObjectC 无法访问并清理它。这是你的理解吗?如果是这样,你有任何相关的链接吗?我当然理解,如果 GC 确定 ObjectA 不可达,那么它也会确定其他两个也不可达。
    • 标记清除、复制或分代垃圾收集器不能以这种方式工作。如果列表 L 包含元素 A..Z 并且那些是指向 A..Z 的 only 指针,并且 A..Z 包含指向 AAA..ZZZ 的指针并且那些 AAA..ZZZ 仅指向由 A..Z 和彼此...然后在清空指向列表 L 跟踪收集器(标记/扫描)的唯一指针后,将不会跟踪 A..Z 或 AAA..ZZZ,并且复制收集器不会复制它们。 Refcounting GC 不会收集指向彼此的元素,直到稍后它们最终进行“主要”(标记/扫描或复制)收集。但是 .NET 不做 refcounting AFAIK..
    【解决方案5】:

    清除列表可以确保如果列表由于某种原因没有被垃圾回收,那么至少它包含的元素仍然可以被处理掉。

    如 cmets 中所述,防止存在对列表的其他引用需要仔细规划,并且在清空列表之前清除该列表不会对性能造成足够大的影响以证明尝试避免这样做是合理的。

    【讨论】:

      猜你喜欢
      • 2015-05-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-04-22
      • 2020-06-05
      • 1970-01-01
      相关资源
      最近更新 更多