类型及其装箱和拆箱
介绍
在 C# 中,数据类型根据它们在内存中存储值的方式进行分类。这为我们提供了三种主要技术,基于这些技术存储值。第一种是值类型,第二种是引用类型,第三种称为指针类型。默认情况下,指针和任何功能都是禁用的,因为它们涉及unsafe关键字,这会突破CLR (公共语言运行时)可以处理的界限。本指南不会详细介绍指针类型。
值类型在其内存分配中保存数据,引用类型包含指向保存实际数据的另一个内存位置的指针。
为了更好地理解,我们将绕道而行,学习栈和堆的重要性。
車輛改道
堆栈是一种特殊类型的内存,分配给值类型数据类型使用,也称为静态内存分配。堆是对应的,这意味着此内存可用于为引用类型数据结构服务的动态内存分配。应用程序可以根据变量使用这两者。但是,作为开发人员,了解这些事实非常重要,因为它们可能会影响应用程序的性能,如果忽略它们,从长远来看会给您带来麻烦。堆栈和堆都存储在计算机的 RAM 中。
值类型
此数据类型将其值或内容保存在堆栈上分配的内存空间中。让我们看一个现实生活中的例子:
int i = 99;
上述语句在幕后的工作方式如下。当使用值99创建i变量时,将直接分配一个内存槽/空间来存储该值。在这种情况下,我们为变量分配另一个值;假设该值兼容或可以转换为此类型。该值直接复制到最初保留的内存位置。预定义数据类型、枚举和结构也是值类型。这些类型是在编译时创建的,通常存储在堆栈内存中。这意味着 GC(垃圾收集器)无法访问它们。
由于分配是在编译时完成的,因此对这种类型的变量和内存的访问速度非常快。堆栈使用 LIFO(后进先出)数据结构。最近保留的块是释放操作发生时的下一个块。当应用程序使用嵌套函数调用或递归函数时,这也是理想的选择。当然,这种递归/嵌套的深度总是有限制的。不过,如今,当语义错误使应用程序陷入深度递归时,堆栈通常会耗尽。
以下数据类型均属于值类型:
- 整数、字节、短整型
- 浮点数、小数、结构体
- 长整型,双精度型,无符号整型
- 字符、枚举、ulong
- bool、sbyte、ushort
引用类型
这种数据结构使用堆将其值存储在内存中。分配在运行时进行,并且由于额外的步骤将指针指向所涉及的实际值,因此对这种数据的访问速度较慢。存储在堆中的项与存储在堆栈中的项不同。这些项之间没有依赖关系,并且可以在任何给定时刻随机访问它们。您可以在运行时使用应用程序自由地分配一个块并释放它!
随之而来的是跟踪堆中哪些部分是空闲的或者在给定时间内被使用的复杂性。
如果开发人员在编译前提前知道应用程序将处理/存储多少数据,他们通常会使用堆栈。大小也很重要,您需要根据要运行应用程序的目标机器进行相对计算。这从来都不是一件容易的事。当我们无法确定需要多少内存时,就会使用堆。或者,根据应用程序在高负载/执行下获得的输入数据和请求,它可以根据需要在运行时保留更多内存。
我们来看一个例子。
string writtenGuide = "Pluralsight";
系统将把变量Pluralsight的值存储在一个位置,并将变量writtenGuide的名称存储在另一个位置,作为指向已分配的原始值的指针。
以下数据类型属于引用类型:
- 所有数组(即使元素是值类型)
- 代表
- 类
- 字符串
传递类型
本节向您展示了传递这两种类型时所产生的效果。
按值分类的值类型
我们以下面的代码为例。
using System;
namespace PassingAround
{
public class PVTBV
{
static void Magic(int a, int b, int c) {
a = b * c;
b = a * c;
c = a * b;
Console.WriteLine($"Inside magic value a: {a}, b: {b}, c: {c}");
}
static void Main(String[] args)
{
int a = 10;
int b = 20;
int c = 30;
Console.WriteLine($"Original value a: {a}, b: {b}, c: {c}");
Magic(a, b, c);
Console.WriteLine($"After magic value a: {a}, b: {b}, c: {c}");
Console.Read();
}
}
}
输出如下。
Original value a: 10, b: 20, c: 30
Inside magic value a: 600, b: 18000, c: 10800000
After magic value a: 10, b: 20, c: 30
传递这些变量的目的是为了显示数据类型的值类型属性。这意味着传递任何值类型的变量并修改该值只会影响传递该值的给定范围内的局部变量,在编译期间,我们的应用程序实际上有 6 个变量保留,其中 3 个在Main函数中,3 个在Magic函数中。调用Magic时,将复制a、b和c变量的值。然后执行计算,并执行WriteLine语句。原始变量没有被修改。
按值引用类型
我们以下面的代码为例。
using System;
namespace PassingAround
{
class Reader {
public int age;
public string platform = "Pluralsight";
}
public class PRTBV
{
static void oneYearLater(Reader a)
{
Console.WriteLine("Time flies...");
a.age += 1;
}
static void Main(String[] args)
{
Reader r1 = new Reader();
Reader r2 = new Reader();
r1.age = 30;
r2.age = 50;
Console.WriteLine($"The age is {r1.age}, favorite platform: {r1.platform}");
Console.WriteLine($"The age is {r2.age}, favorite platform: {r2.platform}");
oneYearLater(r1);
oneYearLater(r2);
Console.WriteLine($"The age is {r1.age}, favorite platform: {r1.platform}");
Console.WriteLine($"The age is {r2.age}, favorite platform: {r2.platform}");
Console.Read();
}
}
}
输出如下。
The age is 30, favorite platform: Pluralsight
The age is 50, favorite platform: Pluralsight
Time flies...
Time flies...
The age is 31, favorite platform: Pluralsight
The age is 51, favorite platform: Pluralsight
这是成功的,因为就年龄修改而言,该类属于引用类型。当我们调用oneYearLater函数时,指针被传递,并且指针指向特定实例的年龄属性的原始值增加了一。这应该让你对引用类型和值类型的主要区别有一个很好的了解。
多线程应用程序
如今,99% 开发的应用程序要么运行在多线程中,要么运行在具有多个核心的环境中。当我们开发多线程应用程序时,我们需要意识到每个线程都有自己的堆栈,但堆将在它们之间共享。从这个意义上讲,堆栈是线程特定的,而堆是应用程序特定的。当我们设计异常处理时,应该考虑线程。
结论
刚开始学习 C# 时,我很快就接触到了引用类型和值类型数据类型,而这些概念并不容易掌握。当我了解了堆和栈后,一切都变得清晰起来,可以说,这些概念造成的漏洞被一扫而光了。你可以不考虑这些概念;但是,如果你想要超越入门级,你就不能忽视 C# 的这个关键部分以及堆和栈内存空间的含义。
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~