C# - 对象范围

本文关键字:范围 对象 | 更新日期: 2023-09-27 17:48:55

我看过这个和这个,我有以下问题,看看我是否正确理解。给定代码

using System;
namespace returnObject
{
    class myObject
    {
        public int number { get; set; }
    }
    class Program
    {
        static void Main(string[] args)
        {
            myObject mainObj = make();
            mainObj.number = 7;
        }
        public static myObject make()
        {
            myObject localObj = new myObject();
            localObj.number = 4;
            return localObj;
        }
    }
}

我希望localObj在 make 方法结束时超出范围,因此,在 main 函数中将 obj.number 设置为 7 会失败。其实不然。我认为我说得对:

  • localObj是对对象的引用
  • 在堆栈上创建localObj
  • localObj在 make 方法结束时超出了范围。
  • localObj引用的对象位于堆上。

那么,我是否正确认为,通常localObj引用的对象会在 make 方法结束时被垃圾回收器标记为删除,但由于引用值已传递回 mainObj ,该对象被引用,因此不符合删除条件?

此外,以这种方式创建对象是否被视为良好做法?

C# - 对象范围

我是否正确认为通常 localObj 引用的对象会在 make 方法结束时被垃圾回收器标记为删除,但由于引用值已传递回 mainObj,因此该对象被引用,因此不符合删除条件?

这是一个极其复杂的问题,但它的形式只承认是或否的答案。与其试图回答这个问题,不如让我按照你原来的观点将其分解为几点。

localObj 是对对象的引用

更好:localObj 是一个局部变量。局部变量是引用类型的存储位置。存储位置包含对对象的引用,或 null。

localObj 是在堆栈上创建的

正确,尽管这是一个实现细节。与变量关联的存储位置是从临时池中分配的。作为实现细节,CLR 使用调用堆栈作为临时池。它不需要;堆栈只是获得临时池的一种廉价、方便的方式。

localObj 在 make 方法结束时超出了范围。

正确。变量的作用域定义为程序文本的区域,可以通过其非限定名称在其中使用它。此特定变量的作用域是方法的整个主体。(我还注意到,在 C# 中,传统的方法、属性和类型以大写字母开头,因为你没有这样做。

localObj 引用的对象位于堆上。

正确。作为实现细节,所有引用要么为 null,要么引用长期存储中的对象,即托管堆。CLR 的实现可以使用流分析来确定特定对象不会转义保存对其唯一引用的局部变量的生存期,因此将其分配给临时池。实际上,这在我所知道的任何实现中都不会发生。

如果未返回,localObj 引用的对象将被垃圾回收器标记为在 make 方法末尾

删除

没有。这是你第一个重大的错误印象。GC 不是确定性的。它不会立即看到局部变量已超出范围,因此对象已被孤立。该对象一直存在,直到某些策略触发垃圾回收。即便如此,该物品也可能在以前的收藏中幸存下来,并被提升到后代。后代的收集频率较低。您所知道的是,该对象将不再标记为生存。 请记住,标记 n-sweep 垃圾回收器将对象标记为生存,而不是删除对象。

此外,在 make 方法结束之前,GC 清理对象是完全合法的。这是处理托管/非托管代码互操作时出现 bug 的常见原因。请考虑以下方案:

void M() 
{
    FileThingy f = GetFileThingy();
    MyUnmanagedLibrary.ConsumeFileThingy(f);
}

f什么时候有资格领取? 只要 GC 可以确定没有托管代码再次使用该引用。但是,在非托管代码获取其引用副本后,没有托管代码会立即使用此引用,因此 GC 有权在调用调用后但在托管代码运行之前立即在另一个线程上收集对象。要解决此问题,您需要使用 KeepAlive 来保持对象处于活动状态。

您不能依赖 GC 在局部变量超出范围时回收存储。 对象的生存期很可能比变量的生存期长,如果 GC 可以证明托管代码无法区分,则对象的生存期更短是合法的。

由于引用值已传递回 mainObj,因此对象被引用,因此不符合删除条件

正确。但同样,假设 mainObj 例程随后没有使用传回的对象进行任何操作。抖动有权注意到这一事实并优化未使用数据的漫游,从而使对象有资格进行早期收集。

我在这里得到的是垃圾收集器应该被认为比你管理内存更聪明。对象不会比它需要的更早消失,也可能比它可能晚,但可能比你想象的要早。停止担心,学会热爱不确定性;GC正在为您管理事情,它做得很好。

C# 不像 C 或 C++ 那样工作;对象永远不会在堆栈上分配。

所有对象(引用类型)都位于垃圾回收堆上;垃圾回收器将在对象不再被引用一段时间后收集对象。

如果不使用弱引用,则基本上不可能在托管对象被 GC'd 后对其进行观察。

你不应该过多地考虑.net框架中的内存管理:
它已被创建,它被称为托管代码,主要是出于这个原因。

如果该对象的引用在不同作用域之间共享,则此

框架足够智能,可以创建一个长生存期对象,如果仅在有限作用域中使用,则创建短生存期对象。

堆栈,堆等是实现细节,你不必关心它们,正如Eric Lippert在几篇博客文章中所说,例如:

  • 堆栈是实现细节,第一部分
  • 堆栈是实现细节,第二部分
  • 关于值类型的真相

然后,如果您需要在某个地方(例如方法)创建一个对象,而在另一个地方使用,请不要担心内存分配......只管去做。

localObj是在

堆上创建的。C# 中的所有引用类型都是。每当使用 new 关键字时,这意味着创建的对象将转到堆。因此,这就是为什么即使从方法返回后,对对象的引用仍然有效。该对象将一直存在,直到垃圾收集器来抓住它。

编辑:重读你的问题后,我明白你来自哪里。是的,引用本身,localObj在堆栈上。这是正确的。但是,引用指向堆上的对象。您返回了该引用的副本,因此新引用仍指向堆上的同一对象。

尽管 localObj 变量超出了堆栈的范围,但它引用了堆上的对象,并且您已将引用作为返回值传递,因此 Main 方法现在引用堆上的同一对象。

垃圾回收器不会清理对象,除非程序不再使用该对象。

如果将此方案与C++进行比较,请将 localObj 视为指针 - 尽管返回指向对象的指针,对象仍保持已分配状态。 注意:C# 托管代码和引用与 C/C++ 不同,因此这只是记住类似行为的一种方式。