C# 值类型与引用类型 详解

C# 值类型与引用类型 详解

C# 值类型与引用类型 完整详解

一、核心本质区别(内存存储)

1. 内存分配位置

  • 值类型(Value Type):变量数据直接存储在栈(Stack)上,变量本身就是数据。
  • 引用类型(Reference Type):实际数据存放在堆(Heap),栈上只存一个内存地址(引用),通过地址指向堆中的对象。

2. 赋值行为差异

  1. 值类型赋值:完整拷贝副本
    赋值时把全部数据复制一份,两个变量完全独立,修改其中一个不会影响另一个。
  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财务计算专用
布尔booltrue/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(值类型)满足全部:
  1. 小型数据(通常实例大小<16字节)
  2. 数据轻量,很少做赋值拷贝
  3. 无需继承、多态
  4. 语义是单一数据点(坐标、颜色、尺寸)
用 class(引用类型)满足任意:
  1. 数据量大
  2. 需要频繁传递、共享对象
  3. 需要继承、多态、接口多实现
  4. 语义是业务实体(用户、订单、商品)

2. ref / out / in 参数(改变值类型传递逻辑)

默认值类型传参是值拷贝,加ref后传递栈变量地址,方法内修改会影响外部变量:

staticvoidModify(refintx){x=999;}inta=10;Modify(refa);// 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,不可nullnull(无堆对象)
内存回收栈自动释放,无GCGC标记清除回收堆内存
继承根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

八、常见踩坑点

  1. 结构体作为List元素修改无效
    List<Point>取出的是结构体副本,直接修改属性不会改变集合内数据,要用索引重新赋值。
  2. 频繁new class产生大量GC
    高频循环内创建类实例会造成堆碎片,可改用结构体或对象池优化。
  3. 字符串拼接性能差
    string不可变,大量拼接用StringBuilder
  4. 拆箱强制转换类型错误抛异常
    装箱是什么类型,拆箱必须对应类型,不能隐式转换。
  5. 数组永远是引用类型
    哪怕数组元素是int值类型,数组本身传递依然共享。