c#中的引用类型和值类型有什么区别?


100

几个月前有人问我这个问题,我无法详细解释。C#中的引用类型和值类型有什么区别?

我知道,价值类型intboolfloat,等和引用类型delegateinterface等等。或者,这是不对的,太?

你能以专业的方式向我解释吗?


3
值得注意的是,我想问的是有关C#的问题,但实际上是关于C#+ .NET的。不分析.NET就无法分析C#。我不会重提这个问题,因为在分析一个问题而不分析另一个问题时可能需要指出几点(迭代器和闭包,我在看着你)
xanatos 2011年

@xanatos是C#,VB.Net和.Net共有的关于CLI的最合适的问题。应该有一个CLI标签,但CLI用于其他标签。有CLR,但这是一个实现,而不是一个标准。
user34660

Answers:


172

你的例子是有点古怪,因为虽然intbool并且float是特定类型,接口和委托是类型的-就像struct并且enum是种价值类型。

我已经在本文中对引用类型和值类型进行解释。我很乐意扩展您感到困惑的所有内容。

“ TL; DR”版本将考虑特定类型的变量/表达式的值是什么。对于值类型,值是信息本身。对于引用类型,该值是一个引用,该引用可以为null,也可以是导航到包含该信息的对象的方式。

例如,将变量想像成一张纸。它上面可以写着“ 5”或“ false”的值,但是它没有我的房子……它必须要有指向我房子的指示。这些方向等同于参考。特别是,两个人可能有不同的纸条,这些纸条上有指向我家的相同指示-如果一个人按照这些指示将我的房子涂成红色,那么第二个人也会看到这种变化。如果他们俩在纸上都只有我的房子的独立图片,那么一个人给他们的纸上色根本不会改变另一个人的纸。


2
重要的是要注意,事物可以提供三种不同的主要语义类型:不可变语义,可变值语义和可变引用语义。从概念上讲,事物实现的语义类型与将其存储为独立堆对象还是变量/字段(结构)正交。实际上,虽然不公开其字段的结构可以实现任何种类的语义,但是.net允许混杂共享堆引用这一事实意味着堆对象无法实现可变值语义。
supercat

我没有得到这一点- while int, bool and float are specific types, interfaces and delegates are kinds of type - just like struct and enum are kinds of value types。int是布尔类型,您是什么意思?C#中的所有内容(例如int,bool,float,类,接口,委托)都是一个类型(准确地说是数据类型)。数据类型在C#中分为“引用类型”和“值类型”。那么,为什么要说int是一种特定类型,而interface是一种类型呢?
RBT

2
@RBT:数据类型不只是分为“引用类型”和“值类型”。它们也被分为“类,结构,枚举,委托,接口”。int是一个结构,string一个类,Action一个委托等。您的“ int,bool,float,类,接口,委托”列表是一个包含不同种类的东西的列表,就像“ 10,int”是包含各种事物的列表。
乔恩·斯基特

@JonSkeet可能这篇文章的答案有点误导人。
RBT

@RBT:我想说的是措辞不佳,但并不可怕。
乔恩·斯基特

26

值类型:

保存一些值而不是内存地址

例:

结构

存储:

TL; DR:变量的值存储在清除位置。例如,局部变量存在于堆栈中,但是当在类中作为成员声明时,它与声明在其中的类紧密地存在于堆中
。Longer因此,值类型存储在声明它们的任何位置。例如:int函数内作为局部变量的in int值将存储在堆栈中,而声明为类成员的in的值将与声明其的类一起存储在堆中。一个类的生命类型与所声明的类完全相同,不需要垃圾收集器进行任何工作。不过,它更复杂,我会参考@JonSkeet的书“ C#In Depth.NET中的内存 ”以提供更简洁的说明。

优点:

值类型不需要额外的垃圾回收。它会与生活在其中的实例一起收集垃圾。方法离开时,方法中的局部变量将被清除。

缺点:

  1. 将大量值传递给方法时,接收变量实际上会复制,因此内存中有两个冗余值。

  2. 由于错过了课程,它失去了所有的益

参考类型:

存放一个不为value的内存地址

例:

存储:

存储在堆上

优点:

  1. 当您将引用变量传递给方法并对其进行更改时,它的确会更改原始值,而在值类型中,将获取给定变量的副本,并且该值也会更改。

  2. 当变量的大小较大时,引用类型是好的

  3. 当类作为引用类型变量时,它们提供了可重用性,从而使面向对象的编程受益

缺点:

分配时需要更多的工作引用,而读取值时需要取消引用。垃圾回收器的额外重载


5
引用类型必须存储在堆中,而值类型必须存储在堆栈中并不一定。如果您想了解更多信息,请阅读yoda.arachsys.com/csharp/memory.html
Rhys

1
这个答案有很多误解。请通过C#阅读Jeff Richters CLR。值类型存储在线程堆栈中,并且不受垃圾回收(GC)的影响-它们与GC无关。引用类型存储在托管堆上,因此必须遵守GC。如果引用类型具有根引用,则无法收集该根引用,并将其提升为0、1和2代。如果它没有根引用,则可以将其收集为垃圾,然后经历称为“复活”的过程。被杀死并恢复生命,最后被收集。
杰里米·汤普森

13

如果您知道计算机如何在内存中分配内容并知道指针是什么,我发现更容易理解两者的区别。

引用通常与指针关联。意味着变量所在的内存地址实际上是将实际对象的另一个内存地址保存在不同的内存位置。

我将要给出的示例过于简化,因此请多加考虑。

想象一下,计算机内存是一排连续的邮政信箱(从邮政信箱0001开始到邮政信箱n),可以在其中存放一些东西。如果邮政信箱不适合您,请尝试使用哈希表或字典或数组或类似方法。

因此,当您执行以下操作时:

var a =“ Hello”;

计算机将执行以下操作:

  1. 分配内存(例如从内存位置1000开始5个字节),然后将H(在1000处),e(在1001处),l(在1002处),l(在1003处)和o(在1004处)放置。
  2. 在内存中分配一个位置(例如在位置0500处)并将其分配为变量a。
    因此有点像别名(0500是a)。
  3. 将该内存位置(0500)的值分配为1000(这是字符串Hello在内存中开始的位置)。因此,变量a保留了 “ Hello”字符串的实际起始存储位置的引用

值类型将实际的东西保存在其内存位置。

因此,当您执行以下操作时:

var a = 1;

计算机将执行以下操作:

  1. 分配一个内存位置,例如0500,并将其分配给变量a(相同的别名)
  2. 将值1放入其中(在内存位置0500)。
    请注意,我们没有分配额外的内存来保存实际值(1)。因此,a实际上保存着实际值,这就是为什么它被称为值类型。


@Jon,嗯,这使我说的话无效了,哈哈。但是就像我说的那样,要在两种类型之间获得一些了解是非常简单的,对我而言,这对我有所帮助。至少我是这样想的:)。
Jimmy Chandra

8

这是大约两年前来自不同论坛的我的帖子。尽管语言是vb.net(与C#相反),但整个.net的“值类型”与“引用类型”概念是统一的,示例仍然适用。

同样重要的是要记住,在.net中,ALL类型从技术上派生自基本类型Object。值类型被设计为具有这种行为,但最后它们还继承了基本类型Object的功能。

A.值类型就是这样-它们表示内存中存储离散值的不同区域。值类型具有固定的内存大小,并存储在堆栈中,堆栈是固定大小的地址的集合。

当您做出这样的声明时:

Dim A as Integer
DIm B as Integer

A = 3
B = A 

您已完成以下操作:

  1. 在内存中创建了2个足以容纳32位整数值的空间。
  2. 在分配给A的内存分配中放置值3
  3. 通过将与分配给A的值相同的值分配给分配给B的内存分配中的值3。

每个变量的值在每个存储位置中离散存在。

B.引用类型可以有各种大小。因此,它们不能存储在“堆栈”中(请记住,堆栈是固定大小的内存分配的集合吗?)。它们存储在“托管堆”中。指向托管堆上每个项目的指针(或“引用”)都保留在堆栈中(如地址)。您的代码使用堆栈中的这些指针来访问存储在托管堆中的对象。因此,当您的代码使用引用变量时,它实际上是在使用指针(或“地址”指向托管堆中的内存位置)。

假设您创建了一个名为clsPerson的类,该类具有字符串Property Person.Name

在这种情况下,当您做出这样的声明时:

Dim p1 As clsPerson
p1 = New clsPerson
p1.Name = "Jim Morrison"

Dim p2 As Person

p2 = p1

在上述情况下,p1.Name属性将返回“ Jim Morrison”,这与您期望的一样。如您所料,p2.Name属性还将返回“ Jim Morrison”。我相信p1和p2都代表堆栈上的不同地址。但是,既然您已经为p2分配了p1的值,则p1和p2都指向托管堆上的SAME LOCATION。

现在考虑这种情况:

Dim p1 As clsPerson
Dim p2 As clsPerson

p1 = New clsPerson
p1.Name = "Jim Morrison"

p2 = p1

p2.Name = "Janis Joplin"

在这种情况下,您在托管堆上创建了一个新的person类实例,并在堆栈上使用了指针p1来引用该对象,并再次为该对象实例的Name属性指定了“ Jim Morrison”的值。接下来,您在堆栈中创建了另一个指针p2,并将其指向托管堆上与p1引用的地址相同的地址(当您进行分配p2 = p1时)。

这是转折。当为p2的Name属性分配值“ Janis Joplin”时,您正在更改p1和p2都引用的对象的Name属性,因此,如果运行以下代码,则:

MsgBox(P1.Name)
'Will return "Janis Joplin"

MsgBox(p2.Name)
'will ALSO return "Janis Joplin"Because both variables (Pointers on the Stack) reference the SAME OBJECT in memory (an Address on the Managed Heap). 

那有意义吗?

持续。如果您这样做:

DIm p1 As New clsPerson
Dim p2 As New clsPerson

p1.Name = "Jim Morrison"
p2.Name = "Janis Joplin"

现在,您有两个不同的Person对象。但是,您再次执行此操作的那一刻:

p2 = p1

您现在已经将两者都指向了“ Jim Morrison”。(我不确定p2所引用的堆上的对象发生了什么……我认为它现在已经超出范围。这是希望有人能使我直觉的那些领域之一。)-编辑:我相信这就是为什么您在进行新分配之前将p2 = Nothing或p2 = New clsPerson设置为原因。

再说一次,如果您现在这样做:

p2.Name = "Jimi Hendrix"

MsgBox(p1.Name)
MsgBox(p2.Name)

两个msgBoxes现在都将返回“ Jimi Hendrix”

这可能会造成一些混乱,最后我要说的是,我可能在某些细节上有误。

祝你好运,希望其他比我更了解的人会来帮助澄清其中的一些问题。。。


我不知道为什么您没有收到任何赞成票。好的答案,通过清晰,简单的示例帮助我理解了。
哈利

至于值类型与引用类型的概念在整个.net中是统一的,它们实际上是在公共语言基础结构(CLI)规范,Ecma标准335(也是ISO标准)中定义的。这是.Net标准部分的标准。Ecma标准334(也是ISO标准)是C#语言,它明确指出C#实现必须依赖CLI或支持以其他方式获得此C#标准所需的最低CLI功能。VB.Net但是不是标准,它是Microsoft专有的。
user34660

5

值数据类型参考数据类型

1) (直接包含数据)但 引用 (指数据)

2)在值中(每个变量都有其自己的副本),但
引用中(超过变量可以引用某些对象)

3)在值上(操作变量不会影响其他变量),但在引用中(变量会影响其他变量)

4) 值类型为(int,bool,float)但 引用类型为(array,class objects,string)


2

值类型:

  • 固定的内存大小。

  • 存储在堆栈存储器中。

  • 保持实际值。

    例如 int,char,bool等...

参考类型:

  • 内存不固定。

  • 存储在堆内存中。

  • 保存实际值的存储器地址。

    例如 字符串,数组,类等...


1

“基于值类型的变量直接包含值。将一个值类型变量分配给另一值将复制包含的值。这与引用类型变量的分配不同,引用类型变量将引用复制到对象,而不是对象本身。” 从微软的图书馆。

您可以在此处此处找到更完整的答案。


1
我不喜欢这种解释,因为听起来引用类型和值类型的赋值方式不同。没有。在这两种情况下,它都会使“目标”变量的值等于表达式-复制该值。该差异是在价值是什么-对于引用类型,这被复制的值是一个参考。但这仍然是变量的值。
乔恩·斯基特

我同意您的要求,并且您已经在本文中阅读到了,我已经知道它可能有所不同。但是,我只是绕过Microsoft关于该主题的指南以及您平时在书中的阅读方式。请不要怪我!:)
Lucas S.

哦,可以肯定。。。有很多MSDN文档,哪里都有问题:)
Jon Skeet

1

有时,解释对于新手尤其无济于事。您可以将值类型想象为数据文件,将引用类型想象为文件的快捷方式。

因此,如果复制参考变量,则仅将链接/指针复制到内存中某个地方的真实数据。如果复制值类型,则实际上是在内存中克隆数据。


0

从神秘的角度来看,这可能是错误的,但为了简单起见:

值类型是通常“按值”传递的值(因此将其复制)。引用类型是通过“引用”传递的(因此提供了指向原始值的指针)。.NET ECMA标准不能保证这些“内容”的保存位置。您可以构建无堆栈的.NET实现,也可以构建无堆的.NET实现(第二个实现非常复杂,但是您可以使用纤维和许多堆栈实现)

结构是值类型(int,bool ...是结构,或者至少模拟为...),类是引用类型。

值类型来自System.ValueType。引用类型从System.Object继承。

现在..最后,您具有值类型,“被引用的对象”和引用(在C ++中,它们称为对象的指针。在.NET中,它们是不透明的。我们不知道它们是什么。从我们的角度来看,它们是是对象的“句柄”)。这些持续时间类似于值类型(它们通过副本传递)。因此,对象由对象(引用类型)和对该对象的零个或多个引用(类似于值类型)组成。当引用数为零时,GC可能会收集它。

通常(在.NET的“默认”实现中),值类型可以在堆栈上(如果它们是本地字段)或在堆上(如果它们是类的字段,如果它们是迭代器函数中的变量),如果它们是闭包引用的变量,如果它们在异步函数中是变量(使用更新的Async CTP)...。引用的值只能进入堆。引用使用与值类型相同的规则。

在值类型由于在迭代器函数,异步函数或闭包引用中而进入堆的情况下,如果您查看编译的文件,则会看到编译器创建了一个类来放置这些变量,并在调用函数时构建该类。

现在,我不知道该怎么写长篇小说,而我一生中还有更好的事情要做。如果您需要“精确的”“学术的”“正确的”版本,请阅读以下内容:

http://blogs.msdn.com/b/ericlippert/archive/2010/09/30/the-truth-about-value-types.aspx

我正在寻找15分钟!它比msdn版本更好,因为它是一篇精简的“即用型”文章。


1
这不仅是深奥的,而且是错误的。这是从根本上错了,我会说-因为引用类型的值仍然被价值,以及通过; 只是值是参考,而不是对象。请参阅pobox.com/~skeet/csharp/parameters.html。哦,局部变量也可以最终出现在堆上,例如,如果它们被捕获或是迭代器块的一部分。
乔恩·斯基特

迭代器块将转换为类,因此“在您后面”它们是“类的字段”。闭包也一样。是的...我忘了写“指针”(引用)和“指针”之间的区别
xanatos 2011年

@xanatos:当然,它们是编译后的类的字段-但它们仍然是源代码中的局部变量。我也不会将引用本身称为“值类型”-我想我知道您来自何处,但是我认为以这种方式弄混水域不是一个好主意。
乔恩·斯基特

@jon是的...它们是第三种类型,因为指针在.net中是“不透明的”,并且它们不是从ValueType派生的。但是它们更类似于值类型而不是引用。您可以“引用”和“淘汰”它们。我不得不弄混水,因为“某人”必须挑剔迭代器的工作。
xanatos 2011年

通过查看我现在指向的文章,我发现:“共有三种值:(1)值类型的实例,(2)引用类型的实例和(3)引用。(C#中的代码无法操作引用类型的实例直接;它总是通过引用来这样做。在不安全的代码中,指针类型被视为值类型,以便确定其值的存储要求
xanatos

0

考虑引用类型的最简单方法是将它们视为“对象ID”。使用对象ID只能执行的操作是创建一个,复制一个,查询或操纵一个对象的类型或比较两个对象是否相等。尝试使用对象ID进行其他任何操作都将视为对该ID所引用的对象执行指示操作的简写。

假设我有两个Car类型的变量X和Y-引用类型。Y碰巧持有“对象ID#19531”。如果我说“ X = Y”,这将导致X保留“对象ID#19531”。请注意,X和Y都不拥有汽车。该汽车(也称为“对象ID#19531”)存储在其他位置。当我将Y复制到X时,我所做的就是复制ID号。现在假设我说X.Color = Colors.Blue。这样的语句将被视为查找“对象ID#19531”并将其涂成蓝色的指令。请注意,即使X和Y现在是指蓝色的汽车而不是黄色的汽车,该语句实际上也不会影响X或Y,因为这两个语句仍然引用“对象ID#19531”,该对象仍与它相同。一直都是。


0

变量类型和参考值易于应用,并且很好地应用于领域模型,有助于开发过程。

为了消除有关“值类型”数量的任何误解,我将评论如何在平台上处理它。NET,特别是在C#(CSharp)中,当被称为APIS时,在我们的方法和函数中按值,按引用发送参数,以及如何正确处理这些值的传递。

阅读本文 C#中的变量类型值和参考


这是仅英语的问答网站,很遗憾,= \。但是,感谢您尝试回答。请创建完整的答案,仅将链接作为辅助手段(而不是完整的持续性答案)。请看看如何回答
杰西

0

假设v是一个值型表达式/变量,并且r是一个引用型表达式/变量

    x = v  
    update(v)  //x will not change value. x stores the old value of v

    x = r 
    update(r)  //x now refers to the updated r. x only stored a link to r, 
               //and r can change but the link to it doesn't .

因此,值类型变量存储实际值(5,或“ h”)。引用类型的变量仅存储指向值所在的隐喻框的链接。


0

在解释C#中可用的不同数据类型之前,重要的是要提到C#是一种强类型语言。这意味着每个变量,常量,输入参数,返回类型,以及通常每个求值为值的表达式都有一个类型。

每种类型都包含将由编译器作为元数据嵌入到可执行文件中的信息,公共语言运行时(CLR)将使用这些信息来保证类型在分配和回收内存时的安全性。

如果您想知道特定类型分配了多少内存,可以使用sizeof运算符,如下所示:

static void Main()
{
    var size = sizeof(int);
    Console.WriteLine($"int size:{size}");
    size = sizeof(bool);
    Console.WriteLine($"bool size:{size}");
    size = sizeof(double);
    Console.WriteLine($"double size:{size}");
    size = sizeof(char);
    Console.WriteLine($"char size:{size}");
}

输出将显示每个变量分配的字节数。

int size:4
bool size:1
double size:8
char size:2

与每种类型有关的信息是:

  • 所需的存储空间。
  • 最大值和最小值。例如,类型Int32接受2147483648和2147483647之间的值。
  • 它继承的基本类型。
  • 在运行时将分配变量内存的位置。
  • 允许的操作种类。
  • 类型包含的成员(方法,字段,事件等)。例如,如果我们检查int类型的定义,我们将找到以下结构和成员:

    namespace System
    {
        [ComVisible(true)]
        public struct Int32 : IComparable, IFormattable, IConvertible, IComparable<Int32>, IEquatable<Int32>
        {      
            public const Int32 MaxValue = 2147483647;     
            public const Int32 MinValue = -2147483648;
            public static Int32 Parse(string s, NumberStyles style, IFormatProvider provider);    
            ... 
        }  
    }

内存管理 当一个操作系统上正在运行多个进程并且RAM的容量不足以容纳所有内存时,操作系统会用RAM映射硬盘的某些部分并开始在硬盘中存储数据。操作系统将使用其中虚拟地址映射到其对应物理地址的特定表来执行请求。这种管理内存的能力称为虚拟内存。

在每个过程中,可用的虚拟内存在以下6部分中进行组织,但是对于本主题的相关性,我们将仅关注堆栈和堆。

堆栈 堆栈是一种LIFO(后进先出)数据结构,其大小取决于操作系统(默认情况下,对于ARM,x86和x64计算机,Windows保留1MB,Linux保留2MB至8MB,具体取决于操作系统)。版)。

这部分内存由CPU自动管理。每次函数声明一个新变量时,编译器都会在堆栈上分配与其大小一样大的新内存块,并且当该函数结束时,将释放该变量的内存块。

此内存区域不是由CPU自动管理的,其大小大于堆栈的大小。当调用new关键字时,编译器将开始寻找适合请求大小的第一个空闲内存块。当找到它时,使用内置的C函数malloc()将其标记为保留,并返回指向该位置的指针。也可以使用内置的C函数free()来释放内存块。这种机制会导致内存碎片,并且必须使用指针来访问正确的内存块,它比堆栈执行读/写操作的速度慢。

自定义和内置类型 C#提供了一组标准的内置类型集,它们代表整数,布尔值,文本字符等,但是您可以使用诸如struct,类,接口和枚举之类的构造来创建自己的类型。

使用struct构造的自定义类型的示例是:

struct Point
{
    public int X;
    public int Y;
};

值和引用类型 我们可以将C#类型分为以下几类:

  • 值类型
  • 参考类型

值类型 值类型从System.ValueType类派生,此类型的变量将其值包含在堆栈的内存分配中。值类型的两个类别是struct和enum。

以下示例显示了布尔类型的成员。如您所见,没有对System.ValueType类的显式引用,这是因为该类由struct继承。

namespace System
{
    [ComVisible(true)]
    public struct Boolean : IComparable, IConvertible, IComparable<Boolean>, IEquatable<Boolean>
    {
        public static readonly string TrueString;
        public static readonly string FalseString;
        public static Boolean Parse(string value);
        ...
    }
}

引用类型 另一方面,引用类型不包含存储在变量中的实际数据,而是包含存储值的堆的内存地址。引用类型的类别是类,委托,数组和接口。

在运行时,声明引用类型变量时,它包含值null,直到将使用关键字new创建的对象分配给它为止。

以下示例显示了通用类型List的成员。

namespace System.Collections.Generic
{
    [DebuggerDisplay("Count = {Count}")]
    [DebuggerTypeProxy(typeof(Generic.Mscorlib_CollectionDebugView<>))]
    [DefaultMember("Item")]
    public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>
    {
        ...
        public T this[int index] { get; set; }
        public int Count { get; }
        public int Capacity { get; set; }
        public void Add(T item);
        public void AddRange(IEnumerable<T> collection);
        ...
    }
}

如果您想查找特定对象的内存地址,则类System.Runtime.InteropServices提供了一种从非托管内存访问托管对象的方法。在下面的示例中,我们将使用静态方法GCHandle.Alloc()为字符串分配句柄,然后使用方法AddrOfPinnedObject检索其地址。

string s1 = "Hello World";
GCHandle gch = GCHandle.Alloc(s1, GCHandleType.Pinned);
IntPtr pObj = gch.AddrOfPinnedObject();
Console.WriteLine($"Memory address:{pObj.ToString()}");

输出将是

Memory address:39723832

参考 官方文档:https : //docs.microsoft.com/zh-cn/cpp/build/reference/stack-stack-allocations?view=vs-2019


-1

标准明确说明了值类型和引用类型之间的差异的许多细节,其中一些不容易理解,特别是对于初学者。

请参阅ECMA标准33,通用语言基础结构(CLI)。CLI也由ISO标准化。我将提供参考,但是对于ECMA,我们必须下载PDF,并且该链接取决于版本号。ISO标准要花钱。

一个区别是值类型可以装箱,而引用类型通常不能装箱。也有例外,但它们非常技术性。

值类型不能具有无参数的实例构造函数或终结器,并且它们不能引用自身。参照本身意味着,例如,如果有一个值类型节点然后的成员节点不能是节点。我认为规范中还有其他要求/限制,但如果这样,它们就不会在一个地方聚集在一起。

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.