几个月前有人问我这个问题,我无法详细解释。C#中的引用类型和值类型有什么区别?
我知道值类型是int
, bool
, float
, etc 而引用类型是delegate
, interface
, 等等。或者这也是错误的吗?
你能用专业的方式给我解释一下吗?
几个月前有人问我这个问题,我无法详细解释。C#中的引用类型和值类型有什么区别?
我知道值类型是int
, bool
, float
, etc 而引用类型是delegate
, interface
, 等等。或者这也是错误的吗?
你能用专业的方式给我解释一下吗?
您的示例有点奇怪,因为 whileint
和bool
是float
特定类型,接口和委托是一种类型 - 就像struct
和enum
是一种值类型。
我在这篇文章中写了对引用类型和值类型的解释。我很乐意扩展您感到困惑的任何内容。
“TL;DR”版本是考虑特定类型的变量/表达式的值是什么。对于值类型,值就是信息本身。对于引用类型,该值是一个引用,它可能为空,或者可能是导航到包含信息的对象的一种方式。
例如,将变量想象成一张纸。它可以写上“5”或“假”的值,但它不能有我的房子……它必须有到我家的方向。这些方向相当于参考。特别是,两个人可能有不同的纸,其中包含相同的指向我家的方向 - 如果一个人按照这些指示将我的房子涂成红色,那么第二个人也会看到这种变化。如果他们俩都在纸上分别有我家的照片,那么一个人给他们的纸上色根本不会改变另一个人的纸。
持有一些价值而不是内存地址
例子:
结构
贮存:
TL;DR : 变量的值存储在它被清除的任何地方。例如,局部变量存在于堆栈中,但当在类中声明为成员时,它存在于与声明它的类紧密耦合的堆上。
更长:因此,值类型存储在它们声明的任何地方。例如:int
作为局部变量的函数内部的值将存储在堆栈中,而int
声明为类中成员的 in 值将与声明它的类一起存储在堆中。值类型 on一个类的生命类型与声明它的类完全相同,几乎不需要垃圾收集器的任何工作。不过它更复杂,我会参考@JonSkeet 的书“ C# In Depth ”.NET 中的内存”以获得更简洁的解释。
好处:
值类型不需要额外的垃圾回收。它与它所在的实例一起收集垃圾。方法中的局部变量在方法离开时被清理。
缺点:
当将大量值传递给方法时,接收变量实际上会复制,因此内存中有两个冗余值。
由于错过了课程。它失去了所有 oop 的好处
保存一个值而不是值的内存地址
例子:
班级
贮存:
存储在堆上
好处:
当您将引用变量传递给方法并且它发生更改时,它确实会更改原始值,而在值类型中,会获取给定变量的副本并且该值会更改。
当变量的大小较大时,引用类型是好的
由于类作为引用类型变量,它们提供了可重用性,从而有利于面向对象的编程
缺点:
在读取 value.extra 垃圾收集器的重载时分配和取消引用时引用更多工作
如果您知道计算机如何在内存中分配内容并知道指针是什么,我发现更容易理解两者的区别。
引用通常与指针相关联。这意味着变量所在的内存地址实际上将实际对象的另一个内存地址保存在不同的内存位置。
我即将给出的示例过于简化,所以请谨慎对待。
想象一下,计算机内存是一排一排的邮政信箱(从邮政信箱 0001 到邮政信箱 n),可以在其中保存一些东西。如果邮政信箱不适合您,请尝试使用哈希表或字典或数组或类似的东西。
因此,当您执行以下操作时:
var a = "你好";
计算机将执行以下操作:
值类型将在其内存位置保存实际事物。
因此,当您执行以下操作时:
变量 a = 1;
计算机将执行以下操作:
这是大约两年前我在另一个论坛上的帖子。虽然语言是 vb.net(而不是 C#),但值类型与引用类型的概念在整个 .net 中是统一的,并且示例仍然有效。
同样重要的是要记住,在 .net 中,所有类型在技术上都派生自基本类型 Object。值类型被设计成这样,但最终它们也继承了基类型 Object 的功能。
A. 值类型只是它们代表内存中存储离散值的不同区域。值类型具有固定的内存大小并存储在堆栈中,堆栈是固定大小的地址的集合。
当你发表这样的声明时:
Dim A as Integer
DIm B as Integer
A = 3
B = A
您已完成以下操作:
每个变量的值离散地存在于每个内存位置。
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 都指向托管堆上的相同位置。
现在考虑这种情况:
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 OR p2 = New clsPerson。
再一次,如果你现在这样做:
p2.Name = "Jimi Hendrix"
MsgBox(p1.Name)
MsgBox(p2.Name)
两个 msgBoxes 现在都将返回“Jimi Hendrix”
这可能有点令人困惑,我会在最后一次说,我可能有一些细节错误。
祝你好运,希望比我更了解的其他人会来帮助澄清其中的一些问题。. .
值数据类型和引用数据类型
1) 值(直接包含数据)但 引用 (指数据)
2)值(每个变量都有自己的副本)但
引用(多个变量可以引用某些对象)
3)在值(操作变量不能影响其他变量)但在引用(变量可以影响其他)
4) 值类型是 (int, bool, float) 但 引用类型是 (array , class objects , string )
值类型:
固定内存大小。
存储在堆栈内存中。
持有实际价值。
前任。int、char、bool 等...
参考类型:
不固定内存。
存储在堆内存中。
保存实际值的内存地址。
前任。字符串、数组、类等...
有时解释对初学者没有帮助。您可以将值类型想象为数据文件,将引用类型想象为文件的快捷方式。
因此,如果您复制引用变量,您只需将链接/指针复制到内存中某处的真实数据。如果你复制一个值类型,你真的克隆了内存中的数据。
这在深奥的方面可能是错误的,但是,为了简单起见:
值类型是通常“按值”传递的值(因此复制它们)。引用类型是“通过引用”传递的(因此给出了指向原始值的指针)。.NET ECMA 标准不能保证这些“东西”的保存位置。您可以构建一个无堆栈或无堆的 .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 版本更好,因为它是一篇浓缩的“即用型”文章。
考虑引用类型的最简单方法是将它们视为“对象 ID”;对对象 ID 唯一可以做的事情是创建一个、复制一个、查询或操作一个的类型,或者比较两个是否相等。尝试使用 object-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”,它仍然是同一辆车一直都是。
变量类型和参考值易于应用并很好地应用于领域模型,方便开发过程。
为了消除关于“价值类型”数量的任何神话,我将评论如何在平台上处理它。NET,特别是在C#(CSharp)中调用APIS并按值、按引用发送参数时,在我们的方法和函数中以及如何正确处理这些值的段落。
阅读这篇文章 C# 中的变量类型值和引用
假设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”)。引用类型变量仅存储指向值所在的隐喻框的链接。
在解释 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
与每种类型相关的信息是:
类型包含的成员(方法、字段、事件等)。例如,如果我们检查类型 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 Point
{
public int X;
public int Y;
};
值和引用类型 我们可以将 C# 类型分为以下几类:
值类型 值类型派生自 System.ValueType 类,并且此类型的变量在堆栈中的内存分配中包含它们的值。两种类型的值类型是 struct 和 enum。
以下示例显示了 boolean 类型的成员。如您所见,没有显式引用 System.ValueType 类,这是因为该类是由结构继承的。
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
标准中明确说明了值类型和引用类型之间差异的许多小细节,其中一些不容易理解,尤其是对于初学者。
请参阅ECMA标准 33,通用语言基础结构 (CLI)。CLI 也由 ISO 标准化。我会提供参考,但对于 ECMA,我们必须下载 PDF,并且该链接取决于版本号。ISO 标准需要花钱。
一个区别是值类型可以装箱,但引用类型通常不能。有例外,但它们是相当技术性的。
值类型不能有无参数的实例构造函数或终结器,它们不能引用自己。例如,引用自身意味着如果存在值类型Node则Node的成员不能是Node。我认为规范中还有其他要求/限制,但如果是这样,那么它们就不会聚集在一个地方。