C# 值类型与引用类型 详解
📅 2026/7/5 8:39:53
👁️ 阅读次数
📝 编程学习
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
底层基于整型,属于值类型
enumColor{Red,Green}// 值类型Colorc1=Color.Red;Colorc2=c1;// 拷贝独立副本c1=Color.Green;// c2不受影响3. 结构体struct
自定义值类型,可包含字段、方法、属性、构造函数
structPoint// 值类型{publicintX;publicintY;}Pointp1=newPoint{X=1,Y=2};Pointp2=p1;// 完整复制X、Yp1.X=100;// p2.X 仍然是 1,互不干扰注意:C# 10+ 支持无参构造函数,结构体默认无参构造永远存在,自动赋0/默认值。
4. 可空值类型Nullable<T>/T?
普通值类型不能为null,包装后允许空:
int?num=null;// Nullable<int>if(num.HasValue){}值类型内存图解
inta=10;intb=a;a=20;栈内存:
栈:a = 10 → 修改后20 栈:b = 10 (独立副本,不受影响)三、引用类型完整分类
所有引用类型直接/间接继承System.Object,数据存堆,栈存引用地址。
1. 类class(最常用)
自定义引用类型,实例分配在堆
classPerson// 引用类型{publicstringName;}Personp1=newPerson{Name="张三"};Personp2=p1;// 仅复制堆地址,指向同一个对象p1.Name="李四";Console.WriteLine(p2.Name);// 输出李四,同步修改内存图解:
栈:p1 → 0x001(堆地址) 栈:p2 → 0x001 堆0x001:{ Name="张三" } → 修改为"李四"2. 字符串string
特殊引用类型,不可变(immutable)
- 属于
class,存在堆; - 一旦创建无法修改,拼接/替换会生成全新字符串;
- 字符串池优化:相同字面量复用地堆内存。
strings1="abc";strings2=s1;s1="xyz";// 新建堆对象,s2仍指向"abc"3. 数组Array
不管元素是值类型还是引用类型,数组本身永远是引用类型
int[]arr1=newint[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,生成引用:
intnum=10;// 栈objectobj=num;// 装箱:堆创建object副本,obj存堆地址开销:分配堆内存、拷贝数据,频繁装箱影响性能。
2. 拆箱(Unboxing):object → 值类型
从堆的object中取出原始值类型数据,复制回栈,必须强制转换:
intnum2=(int)obj;// 拆箱错误示范:类型不匹配会抛InvalidCastException。
避免装箱优化
使用泛型List<T>而非ArrayList,泛型容器不会装箱拆箱。
五、关键易混淆知识点
1. struct vs class 核心选用场景
用 struct(值类型)满足全部:
- 小型数据(通常实例大小<16字节)
- 数据轻量,很少做赋值拷贝
- 无需继承、多态
- 语义是单一数据点(坐标、颜色、尺寸)
用 class(引用类型)满足任意:
- 数据量大
- 需要频繁传递、共享对象
- 需要继承、多态、接口多实现
- 语义是业务实体(用户、订单、商品)
2. ref / out / in 参数(改变值类型传递逻辑)
默认值类型传参是值拷贝,加ref后传递栈变量地址,方法内修改会影响外部变量:
staticvoidModify(refintx){x=999;}inta=10;Modify(refa);// a = 9993. 只读结构体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、数组、委托、接口 |
| 修改传递 | 副本互不干扰 | 一处修改全部同步 |
| 装箱拆箱 | 支持,有性能损耗 | 无需装箱 |
七、完整演示代码
usingSystem;// 值类型:结构体structPoint{publicintX;publicintY;}// 引用类型:类classStudent{publicstringName;}classProgram{staticvoidMain(){// ========== 值类型演示 ==========Pointp1=newPoint{X=10,Y=20};Pointp2=p1;p1.X=999;Console.WriteLine($"值类型 p2.X ={p2.X}");// 10,不受影响// ========== 引用类型演示 ==========Students1=newStudent{Name="小明"};Students2=s1;s1.Name="小红";Console.WriteLine($"引用类型 s2.Name ={s2.Name}");// 小红,同步变化// ========== 装箱拆箱 ==========intnum=100;objectboxObj=num;// 装箱intunboxNum=(int)boxObj;// 拆箱Console.WriteLine($"拆箱结果:{unboxNum}");}}输出:
值类型 p2.X = 10 引用类型 s2.Name = 小红 拆箱结果:100八、常见踩坑点
- 结构体作为List元素修改无效
List<Point>取出的是结构体副本,直接修改属性不会改变集合内数据,要用索引重新赋值。 - 频繁new class产生大量GC
高频循环内创建类实例会造成堆碎片,可改用结构体或对象池优化。 - 字符串拼接性能差
string不可变,大量拼接用StringBuilder。 - 拆箱强制转换类型错误抛异常
装箱是什么类型,拆箱必须对应类型,不能隐式转换。 - 数组永远是引用类型
哪怕数组元素是int值类型,数组本身传递依然共享。
编程学习
技术分享
实战经验