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

时间:2011-02-20 13:21:42

标签: c# .net value-type reference-type

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

我知道价值类型有intboolfloat等,引用类型为delegateinterface等。或者这是错误的呢?

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

15 个答案:

答案 0 :(得分:152)

您的示例有点奇怪,因为intboolfloat是特定类型,接口和委托是类型的 - 就像{ {1}}和struct是各种类型的值。

我已经编写了 对引用类型和值类型in this article的解释。我很乐意扩展你发现令人困惑的任何一点。

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

例如,将变量视为一张纸。它可以写上“5”或“假”的值,但它不能有我的房子......它必须有方向到我的房子。那些方向相当于参考。特别是,两个人可能会有不同的纸张包含相同的方向到我的房子 - 如果一个人按照这些指示并将我的房子涂成红色,那么第二个人也会看到这种变化。如果他们在纸上只有我家的单独的图片,那么一个人给他们的纸张着色不会改变另一个人的纸张。

答案 1 :(得分:22)

值类型:

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

示例:

STRUCT

<强>存储

TL; DR :变量的值存储在被解除的任何位置。例如,局部变量存在于堆栈中,但是当在一个类中作为成员声明它在堆上时,它与所声明的类紧密耦合。 更长因此,值类型会存储在声明的任何位置。 例如:作为局部变量的函数内部的int值将存储在堆栈中,而在类中声明为成员的int值将存储在堆上,其中声明它的类。类上的值类型的生命类型与声明它的类完全相同,几乎不需要垃圾收集器的工作。但它更复杂,我会参考@ JonSkeet的书“C# In Depth”或他的文章“Memory in .NET”来进行更简洁的阐述。

<强>优点:

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

<强>缺点:

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

  2. 因为错过了课程,所以失去了所有的好处

  3. 参考类型:

    保存值不是

    的内存地址

    示例:

    <强>存储

    存储在堆

    <强>优点:

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

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

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

    4. <强>缺点:

      在读取垃圾收集器的value.extra重载时分配和解除引用时的更多工作引用

答案 2 :(得分:13)

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

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

我即将给出的例子非常简单,所以请大家点一下。

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

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

var a =“Hello”;

计算机将执行以下操作:

  1. 分配存储器(例如从存储器位置1000开始5个字节)并且放置H(在1000),e(在1001),1(在1002),1(在1003)和o(在1004)。
  2. 在内存中的某处分配(例如在位置0500处)并将其指定为变量a 所以它有点像别名(0500是a)。
  3. 将该内存位置(0500)的值分配给1000(这是字符串Hello在内存中启动的位置)。因此,变量a将引用保存到“Hello”字符串的实际起始内存位置。
  4. 值类型将实际内容保存在其内存位置。

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

    var a = 1;

    计算机将执行以下操作:

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

答案 3 :(得分:8)

大约两年前,这是来自不同论坛的一篇文章。虽然语言是vb.net(而不是C#),但值类型与引用类型概念在整个.net中是统一的,并且示例仍然成立。

记住在.net中,所有类型从技术上派生自基类型Object也很重要。值类型的设计就是这样,但最后它们还继承了基类型Object的功能。

一个。值类型只是它们 - 它们代表存储器中存储离散VALUE的不同区域。值类型具有固定的内存大小并存储在堆栈中,堆栈是固定大小的地址的集合。

当您发表类似声明时:

Dim A as Integer
DIm B as Integer

A = 3
B = A 

您已完成以下操作:

  1. 在内存中创建了2个空格,足以容纳32位整数值。
  2. 在分配给A
  3. 的内存分配中放置值3
  4. 在分配给B的内存分配中将值3赋值为与A中保存的值相同的值。
  5. 每个变量的值在每个内存位置都是离散存在的。

    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"
    

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

    这就是扭曲。当您为p2的Assign the Name属性赋值“Janis Joplin”时,您将更改两个p1和p2对象REFERENCED的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"
    

    您现在有两个不同的人物对象。但是,你再次这样做的那一刻:

    p2 = p1
    

    你现在已经回到了“吉姆莫里森”。 (我不确定p2所引用的堆上的对象发生了什么......我认为它已经超出了范围。这是有希望的人可以让我直接的那些领域之一......)。 -EDIT:我相信这就是为什么你要在做新的作业之前设置p2 = Nothing OR p2 =新的clsPerson。

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

    p2.Name = "Jimi Hendrix"
    
    MsgBox(p1.Name)
    MsgBox(p2.Name)
    

    msgBoxes现在都将返回“Jimi Hendrix”

    这有点令人困惑,我最后一次说,我可能有一些细节错了。

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

答案 4 :(得分:4)

值数据类型参考数据类型

1)(直接包含数据)     但        参考(指数据)

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

3)(操作变量不能影响其他变量)     但        在参考(变量可能影响其他)

4)值类型是(int,bool,float)     但       引用类型是(数组,类对象,字符串)

答案 5 :(得分:1)

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

您可以找到更完整的答案herehere

答案 6 :(得分:1)

有时解释对初学者尤其有帮助。您可以将值类型设想为数据文件,将引用类型设想为文件的快捷方式。

因此,如果复制引用变量,则只将链接/指针复制到内存中的某个实际数据。如果复制值类型,则确实将数据克隆到内存中。

答案 7 :(得分:0)

这可能是错误的,但是,为了简单起见:

值类型是通常“按值”传递的值(因此复制它们)。引用类型通过“引用”传递(因此给出指向原始值的指针)。 .NET ECMA标准没有保证这些“东西”的保存位置。您可以构建一个无堆栈的.NET实现,或者无堆的.NET实现(第二个非常复杂,但您可能使用光纤和多个堆栈)

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

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

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

一般情况下(在.NET的“默认”实现中),Value类型可以在堆栈上(如果它们是本地字段)或在堆上(如果它们是类的字段,如果它们是一个变量)迭代器函数,如果它们是闭包引用的变量,如果它们在异步函数中是可变的(使用较新的异步CTP)...)。引用的值只能转到堆。引用使用与值类型相同的规则。

在由于它们位于迭代器函数,异步函数或由闭包引用而在堆上的值类型的情况下,如果您观察编译的文件,您将看到编译器创建了一个类到放置这些变量,并在调用函数时构建类。

现在,我不知道如何写长篇大论,我生活中还有更好的事情要做。如果您想要“精确”的“学术”“正确”版本,请阅读以下内容:

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

我需要15分钟才能找到它!它比msdn版本更好,因为它是一篇浓缩的“随时可用”的文章。

答案 8 :(得分: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”,它仍然是与它相同的汽车一直都是。

答案 9 :(得分:0)

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

要删除任何关于“值类型”数量的神话,我将评论如何在平台上处理它。 NET,特别是在C#(CSharp)中调用APIS并按值,通过引用,在我们的方法和函数中发送参数,以及如何正确处理这些值的段落。

阅读这篇文章 Variable Type Value and Reference in C #

答案 10 :(得分: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”)。引用类型变量只存储指向值为隐喻框的链接。

答案 11 :(得分:0)

简单地说,值类型的值和引用类型由它们的引用(内存地址)传递。

这意味着对被调用方法内的值类型参数(形式参数)所做的更改不会反映在调用方法的值(实际参数)中。

但是对被调用方法中的引用参数所做的更改将反映对调用方法中声明的变量的更改。

这是一个简短的解释。请参阅here以详细了解值类型,引用类型以及值类型与引用类型。

答案 12 :(得分:0)

价值类型:

  • 固定内存大小。

  • 存储在堆栈内存中。

  • 保持实际价值。

    例如 int,char,bool等...

参考类型:

  • 未固定内存。

  • 存储在堆内存中。

  • 保存实际值的内存地址。

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

答案 13 :(得分:0)

值类型和引用类型之间没有一个区别,标准明确指出了许多细节,其中一些不易理解,特别是初学者。

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

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

值类型不能包含无参数的实例构造函数或终结函数,并且它们不能引用自身。引用它们自己意味着,例如,如果存在值类型节点,则节点的成员不能是节点。我认为规范中还有其他要求/限制,但如果是这样,那么它们就不会聚集在一起。

答案 14 :(得分: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,class,interface和enum之类的构造来创建自己的类型。

使用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/en-us/cpp/build/reference/stack-stack-allocations?view=vs-2019