一个类型要么是值类型,要么是引用类型。
区别在于拷贝方式:
- 值类型的数据总是拷贝值;内存拷贝;
- 而引用类型的数据总是拷贝引用。引用拷贝;硬链接。
值类型
值类型直接包含值:在内存中,直接开辟空间,存储值。数据存在内存栈(stack)中。
换言之,变量引用的位置就是内存中实际存储值的位置。因此,将一个值赋值给变量1,再将变量1赋值给变量2,会在变量2的内存位置创建值的拷贝,而不是引用变量1的位置。
这进一步造成更变变量1的值不会影响变量2的值。
int a = 3;
int b = a;
a = 5;
Console.WriteLine(b); // 3
类似地,将值类型的实例传给Console.WriteLine()这样的传参数的方法也会生成内存拷贝。即将实参拷贝到形参中去。在方法内部对参数值进行的任何修改都不会影响调用函数中的原始值。
引用类型
引用类型的变量存储对数据存储位置的引用,而不是存储数据。
存储的是内存地址编号,而不是直接存数据。存储的是指向,或者标识牌。
要去指向的那个位置才能找到真正的数据。所以为了访问数据,“运行时”要先从变量中读取内存位置,在“跳转”到包含数据的内存位置。
为引用类型的变量分配实际数据的内存区域称为堆(heap)。
引用类型不像值类型那样要求创建数据的内存拷贝,所以拷贝引用类型的实例比拷贝大的值类型实例更高效。 将引用类型的变量赋给另一个引用类型的变量,只会拷贝引用而不需要拷贝所引用的数据。
由于引用类型只拷贝对数据的引用,所以两个不同的变量可引用相同的数据。如两个变量引用同一个对象,利用一个变量更改对象的字段,用另一个对象访问字段将看到更改结果。无论赋值还是方法调用都会如此。因此,如果在方法内部更改引用类型的数据,控制返回调用者之后,将看到更改后的结果。
有鉴于此,如对象在逻辑上是固定大小、不可变的值,就考虑定义成值类型。如逻辑上是可引用、可变的东西,就考虑定义成引用类型。
引用类型包含:string、数组、自定义类型。
class Program
{
static void Main(string[] args)
{
Student p1 = new Student();
p1.Name = "张三";
Student p2 = p1;
p2.Name = "李四";
Console.WriteLine(p1.Name); // 李四
}
}
public class Student
{
public string _name;
public string Name { get; set; }
}
分析一下:引用类型存储在内存堆中,引用类型的变量存储在栈中,值为堆中数据的地址。引用类型的变量指向数据在堆中实际存储地址。
当执行对p2的赋值之后:
p2.Name = "李四";
对引用类型重新赋值之后,可以参照字符串的不可变性来理解。此时在堆中会重新开辟空间,然后将变量p2指向新的地址,同时由于p2指向的是p1指向的地址。p1也会指向这个新的地址。可以理解为:Linux系统的硬链接。值的修改联动。p2是p1的硬链接。
因此,此时p1.Name的输出就变成了李四。
在VS中打断点,调试看一下:即时窗口可以看到变量在内存中的状态。
补充一: string类型的一个关键特征:不可变性。
string类型也是引用类型,但是上面的变化就要注意了。
string a = "abc";
string b = a;
b = "edc";
Console.WriteLine(a); // abc
Console.WriteLine(b); // edc
补充二:string类型字面值重复性
假如同一字符串字面值在程序集中多次出现,编译器在程序集中只定义字符串一次,且所有变量都指向它。这样一来,假如在代码中多处插入包含大量字符的同一个字符串字面值,最终的程序集只反映其中一个的大小。
总结:
- 值类型在栈中可以直接修改。
- 其赋值给其他变量是在栈中重新开辟新的空间。
- 引用类型在堆中重新开辟空间,用来存储修改的值。
- 引用类型的变量值是引用数据在堆中的地址,
- 其赋值给其他变量是将栈中的值赋值给其他变量。两个引用变量指向相同的堆中地址。