C#值类型和引用类型的区别(非常详细)
C# 中的类型可以分为 4 类,分别是值类型、引用类型、泛型参数和指针类型。本节将介绍值类型和引用类型。
值类型包含大多数的内置类型(具体包括所有数值类型、char 类型和 bool 类型)以及自定义的 struct 类型和 enum 类型。
引用类型包含所有的类、数组、委托和接口类型,其中包括了预定义的 string 类型。
值类型和引用类型最根本的不同在于它们在内存中的处理方式。
可以通过 struct 关键字定义自定义值类型:
值类型实例的赋值总是会进行实例复制,例如:

图 1 赋值操作复制了值类型的实例
例如重新书写前面例子中的 Point 类型,令其成为一个类而非 struct:

图 2 赋值操作复制了引用
相对地,值类型通常不能为 null:
引用类型需要为引用和对象单独分配存储空间。对象除占用了和字段一样的字节数外,还需要额外的管理空间开销。管理开销的精确值本质上属于 .NET 运行时实现的细节,但最少也需要 8 字节来存储该对象类型的键、一些诸如线程锁的状态,以及是否可以被垃圾回收器固定等临时信息。根据 .NET 运行时工作的平台类型(32 位或 64 位平台),每一个对象的引用都需要额外的 4 字节或 8 字节的存储空间。
值类型包含大多数的内置类型(具体包括所有数值类型、char 类型和 bool 类型)以及自定义的 struct 类型和 enum 类型。
引用类型包含所有的类、数组、委托和接口类型,其中包括了预定义的 string 类型。
值类型和引用类型最根本的不同在于它们在内存中的处理方式。
C#值类型
值类型的变量或常量的内容仅仅是一个值。例如,内置的值类型 int 的内容是 32 位的数据。可以通过 struct 关键字定义自定义值类型:
public struct Point { public int X; public int Y; }或采用更简短的形式:
public struct Point { public int X, Y; }
值类型实例的赋值总是会进行实例复制,例如:
Point p1 = new Point(); p1.X = 7; Point p2 = p1; // Assignment causes copy Console.WriteLine(p1.X); // 7 Console.WriteLine(p2.X); // 7 p1.X = 9; // Change p1.X Console.WriteLine(p1.X); // 9 Console.WriteLine(p2.X); // 7下图展示了 p1 和 p2 拥有不同的存储空间。

图 1 赋值操作复制了值类型的实例
C#引用类型
引用类型比值类型复杂,它由对象和对象引用两部分组成。引用类型变量或常量中的内容是一个含值对象的引用。例如重新书写前面例子中的 Point 类型,令其成为一个类而非 struct:
public class Point { public int X, Y; }给引用类型变量赋值只会复制引用,而不是对象实例。这允许不同变量指向同一个对象,而值类型通常不会出现这种情况。如果 Point 是一个类,那么若重复之前的示例,则对 p1 的操作就会影响到 p2 了:
Point p1 = new Point(); p1.X = 7; Point p2 = p1; // Copies p1 reference Console.WriteLine(p1.X); // 7 Console.WriteLine(p2.X); // 7 p1.X = 9; // Change p1.X Console.WriteLine(p1.X); // 9 Console.WriteLine(p2.X); // 9下图展示了 p1 和 p2 是指向同一对象的两个不同引用。

图 2 赋值操作复制了引用
C# null
引用可以用字面量 null 来赋值,表示它并不指向任何对象:Point p = null; Console.WriteLine(p == null); // True // The following line generates a runtime error // (a NullReferenceException is thrown): Console.WriteLine(p.X); class Point {...}
相对地,值类型通常不能为 null:
Point p = null; // Compile-time error int x = null; // Compile-time error struct Point {...}
C#存储开销
值类型实例占用的内存大小就是存储其中的字段所需的内存。例如,Point 需要占用 8 字节的内存:struct Point { int x; // 4 bytes int y; // 4 bytes }从技术上说,CLR 用整数倍字段的大小(最大到8字节)来分配内存地址。因此,下面定义的对象实际上会占用 16 字节的内存(第一个字段的 7 个字节被“浪费了”):
struct A { byte b; long l; }这种行为可以通过指定 StructLayout 特性来重写。
引用类型需要为引用和对象单独分配存储空间。对象除占用了和字段一样的字节数外,还需要额外的管理空间开销。管理开销的精确值本质上属于 .NET 运行时实现的细节,但最少也需要 8 字节来存储该对象类型的键、一些诸如线程锁的状态,以及是否可以被垃圾回收器固定等临时信息。根据 .NET 运行时工作的平台类型(32 位或 64 位平台),每一个对象的引用都需要额外的 4 字节或 8 字节的存储空间。