C# 值类型与引用类型 完整详解
一、核心本质区别(内存存储)
1. 内存分配位置
- 值类型(Value Type) :变量数据直接存储在栈(Stack) 上,变量本身就是数据。
- 引用类型(Reference Type) :实际数据存放在堆(Heap) ,栈上只存一个内存地址(引用),通过地址指向堆中的对象。
2. 赋值行为差异
- 值类型赋值:完整拷贝副本
赋值时把全部数据复制一份,两个变量完全独立,修改其中一个不会影响另一个。 - 引用类型赋值:拷贝地址(浅拷贝)
只复制堆地址,两个变量指向同一个堆对象,任意一个修改对象内容,两边同时变化。
3. 生命周期与回收
- 值类型:栈自动回收,超出作用域立刻销毁,无GC开销。
- 引用类型:靠CLR垃圾回收器(GC)管理堆内存,无任何引用指向对象后才会被GC回收。
4. 默认值
- 值类型:必有默认值 ,不能为
null(可空值类型除外) - 引用类型:默认值是
null,代表栈上没有指向任何堆对象
二、值类型完整分类
所有值类型隐式继承 System.ValueType,而ValueType本身又继承object。
1. 简单内置值类型
| 分类 | 类型 | 说明 |
|---|---|---|
| 整数 | sbyte、byte、short、ushort、int、uint、long、ulong | 固定长度数字 |
| 浮点 | float、double | 小数 |
| 高精度小数 | decimal | 财务计算专用 |
| 布尔 | bool | true/false |
| 字符 | char | 单个Unicode字符 |
2. 枚举 enum
底层基于整型,属于值类型
csharp
enum Color { Red, Green } // 值类型
Color c1 = Color.Red;
Color c2 = c1; // 拷贝独立副本
c1 = Color.Green; // c2不受影响
3. 结构体 struct
自定义值类型,可包含字段、方法、属性、构造函数
csharp
struct Point // 值类型
{
public int X;
public int Y;
}
Point p1 = new Point { X = 1, Y = 2 };
Point p2 = p1; // 完整复制X、Y
p1.X = 100;
// p2.X 仍然是 1,互不干扰
注意:C# 10+ 支持无参构造函数,结构体默认无参构造永远存在,自动赋0/默认值。
4. 可空值类型 Nullable<T> / T?
普通值类型不能为null,包装后允许空:
csharp
int? num = null; // Nullable<int>
if(num.HasValue) {}
值类型内存图解
csharp
int a = 10;
int b = a;
a = 20;
栈内存:
栈:a = 10 → 修改后20
栈:b = 10 (独立副本,不受影响)
三、引用类型完整分类
所有引用类型直接/间接继承 System.Object,数据存堆,栈存引用地址。
1. 类 class(最常用)
自定义引用类型,实例分配在堆
csharp
class Person // 引用类型
{
public string Name;
}
Person p1 = new Person { Name = "张三" };
Person p2 = p1; // 仅复制堆地址,指向同一个对象
p1.Name = "李四";
Console.WriteLine(p2.Name); // 输出李四,同步修改
内存图解:
栈:p1 → 0x001(堆地址)
栈:p2 → 0x001
堆0x001:{ Name="张三" } → 修改为"李四"
2. 字符串 string
特殊引用类型,不可变(immutable)
- 属于
class,存在堆; - 一旦创建无法修改,拼接/替换会生成全新字符串;
- 字符串池优化:相同字面量复用地堆内存。
csharp
string s1 = "abc";
string s2 = s1;
s1 = "xyz"; // 新建堆对象,s2仍指向"abc"
3. 数组 Array
不管元素是值类型还是引用类型,数组本身永远是引用类型
csharp
int[] arr1 = new int[2] {1,2};
int[] arr2 = arr1;
arr1[0] = 99;
Console.WriteLine(arr2[0]); // 99,共享数组
4. 接口 interface
本身不能实例化,但接口变量是引用类型,存储实现类对象的地址。
5. 委托 delegate、事件 event
本质封装方法指针,属于引用类型。
6. 动态类型 dynamic
底层基于object,引用类型。
四、装箱与拆箱(值类型 ↔ object)
1. 装箱(Boxing):值类型 → 引用类型
把栈上的值类型数据,复制到堆中包装为object,生成引用:
csharp
int num = 10; // 栈
object obj = num;// 装箱:堆创建object副本,obj存堆地址
开销:分配堆内存、拷贝数据,频繁装箱影响性能。
2. 拆箱(Unboxing):object → 值类型
从堆的object中取出原始值类型数据,复制回栈,必须强制转换:
csharp
int num2 = (int)obj; // 拆箱
错误示范:类型不匹配会抛InvalidCastException。
避免装箱优化
使用泛型List<T>而非ArrayList,泛型容器不会装箱拆箱。
五、关键易混淆知识点
1. struct vs class 核心选用场景
用 struct(值类型)满足全部:
- 小型数据(通常实例大小<16字节)
- 数据轻量,很少做赋值拷贝
- 无需继承、多态
- 语义是单一数据点(坐标、颜色、尺寸)
用 class(引用类型)满足任意:
- 数据量大
- 需要频繁传递、共享对象
- 需要继承、多态、接口多实现
- 语义是业务实体(用户、订单、商品)
2. ref / out / in 参数(改变值类型传递逻辑)
默认值类型传参是值拷贝,加ref后传递栈变量地址,方法内修改会影响外部变量:
csharp
static void Modify(ref int x)
{
x = 999;
}
int a = 10;
Modify(ref a);
// a = 999
3. 只读结构体 readonly struct
结构体所有字段只读,拷贝时编译器可做优化,减少复制开销。
4. 字符串特殊的相等判断
==:string重载,比较字符内容object.ReferenceEquals(s1,s2):比较是否指向同一个堆地址(判断字符串池复用)
5. 空值区别
- 值类型
int:不能=null;int?才允许null - 引用类型
string:默认null,代表无堆对象
六、对比总结表
| 对比维度 | 值类型(Value Type) | 引用类型(Reference Type) |
|---|---|---|
| 存储位置 | 栈Stack | 数据堆Heap,栈存地址 |
| 赋值逻辑 | 完整复制数据副本 | 仅复制内存地址,共享对象 |
| 默认值 | 数字0、false、\0,不可null | null(无堆对象) |
| 内存回收 | 栈自动释放,无GC | GC标记清除回收堆内存 |
| 继承根 | System.ValueType → object | 直接继承object |
| 代表类型 | struct、enum、int/bool/char等 | class、string、数组、委托、接口 |
| 修改传递 | 副本互不干扰 | 一处修改全部同步 |
| 装箱拆箱 | 支持,有性能损耗 | 无需装箱 |
七、完整演示代码
csharp
using System;
// 值类型:结构体
struct Point
{
public int X;
public int Y;
}
// 引用类型:类
class Student
{
public string Name;
}
class Program
{
static void Main()
{
// ========== 值类型演示 ==========
Point p1 = new Point { X = 10, Y = 20 };
Point p2 = p1;
p1.X = 999;
Console.WriteLine($"值类型 p2.X = {p2.X}"); // 10,不受影响
// ========== 引用类型演示 ==========
Student s1 = new Student { Name = "小明" };
Student s2 = s1;
s1.Name = "小红";
Console.WriteLine($"引用类型 s2.Name = {s2.Name}"); // 小红,同步变化
// ========== 装箱拆箱 ==========
int num = 100;
object boxObj = num; // 装箱
int unboxNum = (int)boxObj; // 拆箱
Console.WriteLine($"拆箱结果:{unboxNum}");
}
}
输出:
值类型 p2.X = 10
引用类型 s2.Name = 小红
拆箱结果:100
八、常见踩坑点
- 结构体作为List元素修改无效
List<Point>取出的是结构体副本,直接修改属性不会改变集合内数据,要用索引重新赋值。 - 频繁new class产生大量GC
高频循环内创建类实例会造成堆碎片,可改用结构体或对象池优化。 - 字符串拼接性能差
string不可变,大量拼接用StringBuilder。 - 拆箱强制转换类型错误抛异常
装箱是什么类型,拆箱必须对应类型,不能隐式转换。 - 数组永远是引用类型
哪怕数组元素是int值类型,数组本身传递依然共享。