为什么.Net书籍为什么谈论堆栈与堆内存分配?


36

似乎每本.net书籍都在谈论值类型与引用类型,并使其指向(通常是错误地)存储每种类型(堆或栈)的状态。通常,它在前几章中介绍,是一些非常重要的事实。我认为认证考试甚至都涵盖了它。为什么(初学者).Net开发人员堆栈与堆甚至重要?您分配的东西就可以了,对吧?


11
有些作者对什么对初学者来说很重要以及什么无关紧要的声音确实有不好的判断。在我最近看到的一本书中,访问修饰符已经提到了内部保护,我在6年的C#中从未使用过...
Timwi

1
我的猜测是,为该部分编写原始.Net文档的人做了很多工作,而该文档正是作者最初基于其书本的内容,然后它就存在了。
格雷格,2010年

说值类型会复制整个内容,而引用则不会,这会更有意义并且更容易理解为什么使用引用,因为这些值的存储位置可能是实现特定的,甚至是不相关的。
特立尼达

采访货运邪教?

Answers:


37

我逐渐确信,这些信息被认为很重要的主要原因是传统。 在非托管环境中,区分堆栈和堆很重要,我们必须手动分配和删除我们使用的内存。现在,垃圾回收负责管理,因此他们忽略了这一点。我认为消息并没有真正得到解决,我们也不必关心使用哪种类型的内存。

正如Fede指出的那样,埃里克·利珀特(Eric Lippert)对此有一些非常有趣的事情要说:http : //blogs.msdn.com/b/ericlippert/archive/2010/09/30/the-truth-about-value-types.aspx

根据这些信息,您可以将我的第一段内容调整为基本上是这样的:“人们之所以包含此信息,并认为它很重要,是因为信息不正确或不完整以及过去需要这些知识。”

对于那些认为出于性能原因仍然很重要的人:如果您确实对事物进行了度量并发现它很重要,那么您将采取什么措施将某些事物从堆中移到栈中?您更有可能找到完全不同的方法来提高问题区域的性能。


6
我听说在某些框架实现中(特别是在Xbox上紧凑),最好在渲染期间(游戏本身)使用结构来减少垃圾回收。您仍然会在其他地方使用普通类型,但会预先分配它们,这样GC就不会在游戏中运行。这是我在.NET中了解的关于堆栈与堆的唯一优化,并且非常适合紧凑框架和实时程序的需求。
CodexArcanum 2010年

5
我大都同意传统的观点。在某些时候,许多经验丰富的程序员可能会使用低级语言进行编程,如果您需要正确而有效的代码,那么这些东西就很重要。但是,以C ++为例,这是一种非托管语言:官方规范实际上并未说自动变量必须放在堆栈上,等等。C++的标准将堆栈和堆视为实现细节。+1
stakx 2010年

36

似乎每本.NET书籍都在谈论值类型与引用类型,并使其指向(通常是错误地)存储每种类型(堆或栈)的状态。通常,它在前几章中介绍,是一些非常重要的事实。

我完全同意; 我一直都这样。

为什么.NET书籍为什么谈论堆栈与堆内存分配?

原因之一是因为许多人都是从C或C ++的背景来学习C#(或其他.NET语言)的。由于这些语言不会为您强制执行有关存储寿命的规则,因此您需要了解这些规则并仔细实施程序以遵循它们。

现在,了解这些规则并在C中遵循它们并不需要您了解“堆”和“堆栈”。但是,如果您确实了解数据结构的工作原理,那么通常更容易理解和遵循规则。

当写一个初学者的书是很自然的作者解释的概念以相同的顺序是他们学会了他们。这不一定对用户有意义。我最近是Scott Dorman的C#4初学者书籍的技术编辑,而我喜欢它的一件事是Scott为主题选择了一个非常合理的排序,而不是从内存管理的真正高级主题入手。

原因的另一部分是MSDN文档中的某些页面强烈强调存储注意事项。尤其是早期的MSDN文档,至今仍在使用。该文档中的许多都存在从未被删除的细微错误,您必须记住它是在历史上的特定时间针对特定受众编写的。

为什么(初学者).NET开发人员堆栈与堆甚至重要?

我认为事实并非如此。更重要的是要了解以下内容:

  • 引用类型和值类型在复制语义上有什么区别?
  • “ ref int x”参数的行为如何?
  • 为什么值类型应该是不可变的?

等等。

您分配的东西就可以了,对吧?

那是理想。

现在,在某些情况下它确实很重要。垃圾收集很棒而且相对便宜,但是它不是免费的。在周围复制小型结构相对便宜,但并非免费。在现实的性能场景中,您必须平衡收集压力的成本与过度复制的成本。在这些情况下,深入了解所有相关内存的大小,位置和实际寿命非常有帮助。

同样,在现实的互操作方案中,有必要了解堆栈中的内容和堆中的内容以及垃圾回收器可能在移动的内容。这就是C#具有“固定”,“堆栈分配”等功能的原因。

但是这些都是高级方案。理想情况下,初学者程序员无需担心这些东西。


2
感谢您的回答埃里克。您最近关于该主题的博客文章实际上是促使我发布该问题的原因。
格雷格2010年

13

你们都错过了重点。堆栈/堆区别之所以重要的原因在于范围

struct S { ... }

void f() {
    var x = new S();
    ...
 }

一旦X去的范围时,所创建的对象断然消失了。那只是因为它是在堆栈上分配的,而不是在堆上分配的。方法的“ ...”部分中没有任何内容可以改变这一事实。特别是,任何赋值或方法调用都只能复制 S结构,而不能创建新的引用以使其得以存活。

class C { ... }

void f() {
     var x = new C();
     ...
}

完全不同的故事!由于x现在位于堆上,因此x超出范围后,它的对象(即对象本身,而不是其副本)可能会继续存在。实际上,它不会继续存在的唯一方法是x是对其的唯一引用。如果“ ...”部分中的赋值或方法调用创建了其他引用,这些引用在x超出范围时仍处于“活动”状态,则该对象将继续存在。

那是一个非常重要的概念,真正理解“什么和为什么”的唯一方法是知道堆栈和堆分配之间的区别。


我不确定我以前在书中是否曾提出过这种论点以及堆栈/堆的讨论,但这是一个很好的论点。+1
Greg

2
由于C#生成闭包的方式,中的代码...可能会导致x转换为编译器生成的类的字段,从而超出了指定范围。就个人而言,我觉得隐式提升的想法令人反感,但是语言设计者似乎会这样做(而不是要求任何被提升的变量在其声明中都必须声明某些内容)。为了确保程序的正确性,通常必须考虑对象可能存在的所有引用。知道到例程返回时,将不会保留任何传入引用的副本。
2012年

1
对于“结构在堆栈上”,正确的说法是,如果将存储位置声明为structType foo,则该存储位置foo将保留其字段的内容;如果foo在堆栈上,则其字段也是如此。如果foo在堆上,则其字段也是如此。如果foo位于联网的Apple II中,则其字段也是如此。相反,如果foo是类类型,则它将包含null或对对象的引用foo可以说class-type 持有对象字段的唯一情况是,如果它是类的唯一字段,并且持有对自身的引用。
2012年

+1,我喜欢您在这里的见识,我认为这是有效的...但是,我不认为书为什么如此深入地涵盖这一主题是合理的。似乎您在这里所解释的内容可以取代所述书的那三章或四章,并且会更有帮助。
Frank V

1
据我所知,结构不是必须的,它们也不一定总是放在堆栈上。
2016年

5

至于为什么要覆盖这个主题,我同意@Kirk的观点,这是一个重要的概念,您必须理解。您对这些机制了解得越多,就可以做得更好,使出色的应用程序平稳运行。

现在,埃里克·利珀特(Eric Lippert)似乎同意您的观点,即大多数作者未正确涵盖该主题。我建议您阅读他的博客,以更好地了解其内幕。


2
埃里克(Eric)的文章指出,您需要了解的只是值和引用类型的明显特征,并且不应指望实现会保持不变。我认为这是一个很不切实际的建议,它提出了一种无需堆栈即可重新实现C#的有效方法,但他的观点是正确的:它不是语言规范的一部分。因此,使用我能想到的这种解释的唯一原因是它的寓言对了解其他语言(尤其是C)的程序员有用。只要他们知道它的寓言,许多文献都不清楚。
杰里米

5

好吧,我认为这就是托管环境的重点。我什至会称其为基础运行时的实现细节,您不应对此做任何假设,因为它随时可能更改。

我对.NET不太了解,但是据我所知,它在执行之前就已JITted了。例如,JIT可以执行转义分析,而不能执行转义分析,突然之间,您会将对象放在堆栈上或仅放在某些寄存器中。你不知道这一点。

我想有些书涵盖了它,仅仅是因为作者对此非常重视,或者是因为他们假设听众会这么做(例如,如果您编写了“ C ++程序员C#”,则可能应该涵盖该主题)。

尽管如此,我认为没有什么可以说是“管理内存”了。否则,人们可能得出错误的结论。


2

您必须了解内存分配如何有效地使用它,即使您不必显式管理它。这几乎适用于计算机科学中的每个抽象。


2
在托管语言中,您确实必须知道值类型和引用类型之间的区别,但除此之外,考虑到如何在后台对其进行管理很容易陷入困境。参见示例:stackoverflow.com/questions/4083981/…–
罗伯特·哈维

我必须同意罗伯特的

堆分配与堆栈分配之间的差异正是解释值与引用类型之间差异的原因。
杰里米


1
杰里米,堆和栈分配之间的差异无法解释值类型和引用类型之间的不同的行为,因为有些时候,这两个值类型和引用类型都在堆上,但他们的行为不同。例如,当您需要将引用传递用于引用类型与值类型时,需要理解的是更重要的事情。这仅取决于“是值类型还是引用类型”,而不是“是否在堆上”。
Tim Goodman 2010年

2

在某些极端情况下,它可能会有所作为。默认堆空间为1meg,而堆为数gig。因此,如果您的解决方案包含大量对象,则可能会耗尽堆栈空间,同时又有大量堆空间。

但是,在大多数情况下,这是相当学术的。


是的,但是我怀疑这些书都很难解释引用本身是存储在堆栈中的-因此,如果您有很多引用类型或很多值类型,您仍然会有堆栈溢出的问题。
杰里米

0

如您所说,C#应该抽象化内存管理,而堆与栈分配是实现细节,从理论上讲,开发人员无需了解这些细节。

问题是,如果不参考这些实现细节,确实很难以直观的方式解释某些事情。尝试解释可变值类型时的可观察行为-如果不参考堆栈/堆的区别,几乎是不可能做到的。还是尝试解释为什么在语言中首先具有值类型,以及何时使用它们?您需要了解区别才能理解语言。

请注意,即使说Python或JavaScript的书即使提及也没什么大不了的。这是因为一切都是堆分配的或不可变的,这意味着不同的复制语义永远不会发挥作用。在那些语言中,内存的抽象是可行的,在C#中是泄漏的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.