几个月前有人问我这个问题,我无法详细解释。C#中的引用类型和值类型有什么区别?
我知道,价值类型int
,bool
,float
,等和引用类型delegate
,interface
等等。或者,这是不对的,太?
你能以专业的方式向我解释吗?
几个月前有人问我这个问题,我无法详细解释。C#中的引用类型和值类型有什么区别?
我知道,价值类型int
,bool
,float
,等和引用类型delegate
,interface
等等。或者,这是不对的,太?
你能以专业的方式向我解释吗?
Answers:
你的例子是有点古怪,因为虽然int
,bool
并且float
是特定类型,接口和委托是种类型的-就像struct
并且enum
是种价值类型。
我已经在本文中对引用类型和值类型进行了解释。我很乐意扩展您感到困惑的所有内容。
“ TL; DR”版本将考虑特定类型的变量/表达式的值是什么。对于值类型,值是信息本身。对于引用类型,该值是一个引用,该引用可以为null,也可以是导航到包含该信息的对象的方式。
例如,将变量想像成一张纸。它上面可以写着“ 5”或“ false”的值,但是它没有我的房子……它必须要有指向我房子的指示。这些方向等同于参考。特别是,两个人可能有不同的纸条,这些纸条上有指向我家的相同指示-如果一个人按照这些指示将我的房子涂成红色,那么第二个人也会看到这种变化。如果他们俩在纸上都只有我的房子的独立图片,那么一个人给他们的纸上色根本不会改变另一个人的纸。
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是一种类型呢?
int
是一个结构,string
一个类,Action
一个委托等。您的“ int,bool,float,类,接口,委托”列表是一个包含不同种类的东西的列表,就像“ 10,int”是包含各种事物的列表。
保存一些值而不是内存地址
例:
结构
存储:
TL; DR:变量的值存储在清除位置。例如,局部变量存在于堆栈中,但是当在类中作为成员声明时,它与声明在其中的类紧密地存在于堆中
。Longer:因此,值类型存储在声明它们的任何位置。例如:int
函数内作为局部变量的in int
值将存储在堆栈中,而声明为类成员的in的值将与声明其的类一起存储在堆中。一个类的生命类型与所声明的类完全相同,不需要垃圾收集器进行任何工作。不过,它更复杂,我会参考@JonSkeet的书“ C#In Depth ”“ .NET中的内存 ”以提供更简洁的说明。
优点:
值类型不需要额外的垃圾回收。它会与生活在其中的实例一起收集垃圾。方法离开时,方法中的局部变量将被清除。
缺点:
将大量值传递给方法时,接收变量实际上会复制,因此内存中有两个冗余值。
由于错过了课程,它失去了所有的益
存放一个不为value的内存地址
例:
类
存储:
存储在堆上
优点:
当您将引用变量传递给方法并对其进行更改时,它的确会更改原始值,而在值类型中,将获取给定变量的副本,并且该值也会更改。
当变量的大小较大时,引用类型是好的
当类作为引用类型变量时,它们提供了可重用性,从而使面向对象的编程受益
缺点:
分配时需要更多的工作引用,而读取值时需要取消引用。垃圾回收器的额外重载
如果您知道计算机如何在内存中分配内容并知道指针是什么,我发现更容易理解两者的区别。
引用通常与指针关联。意味着变量所在的内存地址实际上是将实际对象的另一个内存地址保存在不同的内存位置。
我将要给出的示例过于简化,因此请多加考虑。
想象一下,计算机内存是一排连续的邮政信箱(从邮政信箱0001开始到邮政信箱n),可以在其中存放一些东西。如果邮政信箱不适合您,请尝试使用哈希表或字典或数组或类似方法。
因此,当您执行以下操作时:
var a =“ Hello”;
计算机将执行以下操作:
值类型将实际的东西保存在其内存位置。
因此,当您执行以下操作时:
var a = 1;
计算机将执行以下操作:
这是大约两年前来自不同论坛的我的帖子。尽管语言是vb.net(与C#相反),但整个.net的“值类型”与“引用类型”概念是统一的,示例仍然适用。
同样重要的是要记住,在.net中,ALL类型从技术上派生自基本类型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都指向托管堆上的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”
这可能会造成一些混乱,最后我要说的是,我可能在某些细节上有误。
祝你好运,希望其他比我更了解的人会来帮助澄清其中的一些问题。。。
值数据类型和参考数据类型
1) 值(直接包含数据)但 引用 (指数据)
2)在值中(每个变量都有其自己的副本),但
在引用中(超过变量可以引用某些对象)
3)在值上(操作变量不会影响其他变量),但在引用中(变量会影响其他变量)
4) 值类型为(int,bool,float)但 引用类型为(array,class objects,string)
从神秘的角度来看,这可能是错误的,但为了简单起见:
值类型是通常“按值”传递的值(因此将其复制)。引用类型是通过“引用”传递的(因此提供了指向原始值的指针)。.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版本更好,因为它是一篇精简的“即用型”文章。
考虑引用类型的最简单方法是将它们视为“对象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”,该对象仍与它相同。一直都是。
变量类型和参考值易于应用,并且很好地应用于领域模型,有助于开发过程。
为了消除有关“值类型”数量的任何误解,我将评论如何在平台上处理它。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构造的自定义类型的示例是:
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