65.9K
CodeProject 正在变化。 阅读更多。
Home

六个重要的 .NET 概念:堆栈、堆、值类型、引用类型、装箱和拆箱

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (412投票s)

2010年4月27日

CPOL

7分钟阅读

viewsIcon

1684740

downloadIcon

3030

堆栈、堆、值类型、引用类型、装箱和拆箱简介

目录

如果你不想阅读我的完整文章,可以观看下面的视频,了解堆栈与堆 | 装箱与拆箱 | 值类型与引用类型。

 

如需进一步阅读,请观看下面的面试准备视频和逐步视频系列。

引言

本文将解释六个重要的概念:堆栈、堆、值类型、引用类型、装箱和拆箱。本文首先解释声明变量时内部发生了什么,然后继续解释两个重要的概念:堆栈和堆。接着,文章讨论了引用类型和值类型,并澄清了它们的一些重要基本原理。

文章最后通过示例代码演示了装箱和拆箱如何影响性能。

声明变量时内部发生了什么?

在 .NET 应用程序中声明变量时,它会在 RAM 中分配一块内存。这块内存包含三样东西:变量的名称、变量的数据类型和变量的值。

这是关于内存中发生情况的简单解释,但根据数据类型,你的变量会被分配相应类型的内存。有两种类型的内存分配:堆栈内存和堆内存。在接下来的章节中,我们将尝试更详细地理解这两种内存类型。

堆栈和堆

为了理解堆栈和堆,让我们了解下面的代码内部实际上发生了什么。

public void Method1()
{
    // Line 1
    int i=4;

    // Line 2
    int y=2;

    //Line 3
    class1 cls1 = new class1();
}

这是一段三行代码,让我们逐行了解事情在内部是如何执行的。

内存分配和解除分配使用 LIFO(后进先出)逻辑完成。换句话说,内存只在内存的一端(即堆栈的顶部)进行分配和解除分配。

  • 第 1 行:当这一行执行时,编译器会在堆栈中分配少量内存。堆栈负责跟踪应用程序中所需的运行内存。
  • 第 2 行:现在执行进入下一步。正如“堆栈”这个名称所暗示的,它将这块内存分配堆叠在第一块内存分配之上。你可以把堆栈想象成一系列相互堆叠的隔间或盒子。
  • 第 3 行:在第 3 行,我们创建了一个对象。当这一行执行时,它会在堆栈上创建一个指针,而实际的对象则存储在一种不同类型的内存位置,称为“堆”。“堆”不跟踪运行内存,它只是一堆对象,可以随时访问。堆用于动态内存分配。

这里还需要注意一个重要点,引用指针是在堆栈上分配的。语句 `Class1 cls1;` 不会为 `Class1` 的实例分配内存,它只分配一个堆栈变量 `cls1`(并将其设置为 `null`)。当它遇到 `new` 关键字时,它才在“堆”上分配。

退出方法(有趣的部分):现在,执行控制最终开始退出方法。当它通过结束控制时,它会清除所有在堆栈上分配的内存变量。换句话说,所有与 `int` 数据类型相关的变量都以“LIFO”方式从堆栈中解除分配。

问题——它没有解除分配堆内存。这部分内存稍后将由垃圾回收器解除分配。

现在,我们的许多开发者朋友一定在想,为什么要两种类型的内存,我们难道不能只在一种内存类型上分配所有东西就搞定了吗?

如果你仔细观察,基本数据类型不复杂,它们只存储单个值,如“`int i = 0`”。对象数据类型是复杂的,它们引用其他对象或其他基本数据类型。换句话说,它们持有对其他多个值的引用,并且每个值都必须存储在内存中。对象类型需要动态内存,而基本类型需要静态内存。如果需要动态内存,它会在堆上分配,否则它会在堆栈上分配。

值类型和引用类型

现在我们已经理解了堆栈和堆的概念,是时候理解值类型和引用类型的概念了。值类型是指数据和内存都存储在同一位置的类型。引用类型有一个指针,指向内存位置。

下面是一个简单的整数数据类型,名为 `i`,其值被赋值给另一个名为 `j` 的整数数据类型。这两个内存值都分配在堆栈上。

当我们把一个 `int` 值赋给另一个 `int` 值时,它会创建一个完全不同的副本。换句话说,如果你改变其中任何一个,另一个都不会改变。这类数据类型被称为“值类型”。

当我们创建一个对象并将一个对象赋值给另一个对象时,它们都指向同一个内存位置,如下图所示的代码片段。所以当我们把 `obj` 赋值给 `obj1` 时,它们都指向同一个内存位置。

换句话说,如果我们改变其中一个,另一个对象也会受到影响;这被称为“引用类型”。

那么哪些数据类型是引用类型,哪些是值类型?

在 .NET 中,根据数据类型,变量要么分配在堆栈上,要么分配在堆上。`String` 和 `Objects` 是引用类型,而任何其他 .NET 基本数据类型都分配在堆栈上。下图更详细地解释了这一点。

装箱和拆箱

哇,你分享了这么多知识,那么在实际编程中有什么用呢?其中一个最大的影响是理解由于数据在堆栈和堆之间来回移动而造成的性能损失。

考虑下面的代码片段。当我们将值类型转换为引用类型时,数据从堆栈移动到堆。当我们将引用类型转换为值类型时,数据从堆移动到堆栈。

这种数据在堆和堆栈之间来回移动会造成性能损失。

当数据从值类型移动到引用类型时,这被称为“装箱”,反之则被称为“拆箱”。

如果你编译上面的代码并在 ILDASM 中查看,你可以在 IL 代码中看到“装箱”和“拆箱”的样子。下图演示了这一点。

装箱和拆箱的性能影响

为了查看性能如何受到影响,我们运行了以下两个函数 10,000 次。一个函数包含装箱,另一个函数很简单。我们使用秒表对象来监测所花费的时间。

装箱函数执行了 3542 毫秒,而没有装箱的代码执行了 2477 毫秒。换句话说,尽量避免装箱和拆箱。在一个你需要装箱和拆箱的项目中,只有在绝对必要时才使用它。

本文附带了示例代码,演示了这种性能影响。

目前,我还没有包含拆箱的源代码,但其原理是相同的。你可以编写代码并使用 `stopwatch` 类进行实验。

关于源代码

文章附带了一个简单的代码,演示了装箱如何造成性能影响。你可以在这里下载源代码。

历史

  • 2010 年 4 月 27 日:初始版本
  • 2021 年 6 月 24 日:更新
© . All rights reserved.