C# 值类型与引用类型详解:存储位置、赋值机制、参数传递、相等比较、装箱拆箱与常见陷阱

C# 值类型与引用类型详解:存储位置、赋值机制、参数传递、相等比较、装箱拆箱与常见陷阱

在开始之前首先祝大家中秋快乐!! 一句话核心结论:值类型"存值"------赋值时把整份数据复制一份,两份互不影响;引用类型"存地址"------赋值只复制引用,多个变量指向堆上同一个对象。


目录

  1. 前置知识:栈与堆(内存两大区域)
  2. 定义与类型体系
  3. 存储位置与赋值机制
  4. 参数传递
  5. 相等性比较
  6. 装箱与拆箱
  7. 常见陷阱
  8. 判断口诀
  9. 附录:速查表

0. 前置知识:栈与堆(内存两大区域)

C# 的内存分为两大区域:栈 和堆 。核心区别可以一句话概括------值类型"住"在栈上,引用类型的"门牌号(引用)"在栈上、"房子(对象本体)"在堆上。

栈 Stack 堆 Heap
存放 局部值类型变量、引用变量的地址 class 实例、数组、对象本体
生命周期 方法执行完自动释放 GC 自动回收,时机不确定
分配速度 很快 相对慢
赋值行为 值类型直接复制内容 复制地址,指向同一个堆对象

展开讲:

  • 栈(Stack) :线程私有、后进先出。方法调用时局部变量和参数自动入栈,方法返回时自动出栈。速度极快(只需移动栈指针),但容量小(默认约 1MB),递归过深会抛 StackOverflowException。由编译器自动管理,不需要 GC。
  • 堆(Heap) :进程共享、运行时动态分配。new 出来的对象(class、string、数组、委托)都在这里。分配较慢(要寻找空闲块),生命周期不确定,由**垃圾回收器(GC)**分代回收。容量大,但存在内存碎片和 GC 停顿成本。
  • 最容易混淆的点 :int x = 42 是值类型,直接放栈上;Person p = new Person() 中 p 这个引用(地址)在栈上,而 Person 对象本体在堆上。所以"引用类型"指的是"变量里存的是引用",并不是"整个对象都在栈上"。
  • 值/引用语义 :值类型赋值是复制一份(互不影响);引用类型赋值只复制引用(指向同一个对象,互相影响)。另外,值类型转 object 会发生装箱,在堆上生成一个副本。

【插图:栈与堆内存分布示意图】

风险:递归过深 → StackOverflowException

递归会反复调用自身、不断压入栈帧。递归层数太多时栈空间被填满,就会直接抛出栈溢出异常 StackOverflowException。

GC 分代回收(Gen0 / Gen1 / Gen2)

GC 的核心机制是分代回收:

  • Gen0(0 代):新创建的对象,回收频率最高;大部分对象都在 0 代被回收。
  • Gen1(1 代):0 代回收后存活下来的对象,作为 0 代和 2 代之间的缓冲区。
  • Gen2(2 代):长期存活的对象/大对象,回收代价最高,很少触发。

一次完整回收分为三步:标记 → 清除 → 压缩。

  1. 标记:从栈、静态变量等"根引用"出发,找出所有还在被使用的对象;
  2. 清除:把没有标记的垃圾对象所占内存标记为空闲;
  3. 压缩:移动存活对象、消除内存碎片(移动对象会同步修改引用地址)。

一句话记忆:栈"自动、快、小、线程私有";堆"动态、慢、大、进程共享"。 写完代码多想一想------"这个变量是被拷贝了,还是被共享了"。


1. 定义与类型体系

1.1 什么是值类型

值类型(Value Type)的变量直接保存数据本身。赋值给另一个变量时,复制的是完整的一份数据。

csharp 复制代码
int a = 10;
int b = a;        // b 得到 a 的"副本"
b = 99;
Console.WriteLine(a);   // 输出 10(a 不受影响)

1.2 什么是引用类型

引用类型(Reference Type)的变量保存的是对象的地址(引用)。对象本身存放在托管堆上,赋值时只复制地址,不复制对象。

csharp 复制代码
var a = new List<int> { 1, 2, 3 };
var b = a;            // a 与 b 指向同一个对象
b.Add(4);
Console.WriteLine(a.Count);   // 输出 4(a、b 共享同一个对象)

1.3 类型体系

所有类型最终都继承自 object。值类型经 System.ValueType 间接继承,引用类型则直接或间接继承 object。
#mermaid-svg-otHb88opK93k3lCq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-otHb88opK93k3lCq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-otHb88opK93k3lCq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-otHb88opK93k3lCq .error-icon{fill:#552222;}#mermaid-svg-otHb88opK93k3lCq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-otHb88opK93k3lCq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-otHb88opK93k3lCq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-otHb88opK93k3lCq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-otHb88opK93k3lCq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-otHb88opK93k3lCq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-otHb88opK93k3lCq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-otHb88opK93k3lCq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-otHb88opK93k3lCq .marker.cross{stroke:#333333;}#mermaid-svg-otHb88opK93k3lCq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-otHb88opK93k3lCq p{margin:0;}#mermaid-svg-otHb88opK93k3lCq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-otHb88opK93k3lCq .cluster-label text{fill:#333;}#mermaid-svg-otHb88opK93k3lCq .cluster-label span{color:#333;}#mermaid-svg-otHb88opK93k3lCq .cluster-label span p{background-color:transparent;}#mermaid-svg-otHb88opK93k3lCq .label text,#mermaid-svg-otHb88opK93k3lCq span{fill:#333;color:#333;}#mermaid-svg-otHb88opK93k3lCq .node rect,#mermaid-svg-otHb88opK93k3lCq .node circle,#mermaid-svg-otHb88opK93k3lCq .node ellipse,#mermaid-svg-otHb88opK93k3lCq .node polygon,#mermaid-svg-otHb88opK93k3lCq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-otHb88opK93k3lCq .rough-node .label text,#mermaid-svg-otHb88opK93k3lCq .node .label text,#mermaid-svg-otHb88opK93k3lCq .image-shape .label,#mermaid-svg-otHb88opK93k3lCq .icon-shape .label{text-anchor:middle;}#mermaid-svg-otHb88opK93k3lCq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-otHb88opK93k3lCq .rough-node .label,#mermaid-svg-otHb88opK93k3lCq .node .label,#mermaid-svg-otHb88opK93k3lCq .image-shape .label,#mermaid-svg-otHb88opK93k3lCq .icon-shape .label{text-align:center;}#mermaid-svg-otHb88opK93k3lCq .node.clickable{cursor:pointer;}#mermaid-svg-otHb88opK93k3lCq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-otHb88opK93k3lCq .arrowheadPath{fill:#333333;}#mermaid-svg-otHb88opK93k3lCq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-otHb88opK93k3lCq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-otHb88opK93k3lCq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-otHb88opK93k3lCq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-otHb88opK93k3lCq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-otHb88opK93k3lCq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-otHb88opK93k3lCq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-otHb88opK93k3lCq .cluster text{fill:#333;}#mermaid-svg-otHb88opK93k3lCq .cluster span{color:#333;}#mermaid-svg-otHb88opK93k3lCq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-otHb88opK93k3lCq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-otHb88opK93k3lCq rect.text{fill:none;stroke-width:0;}#mermaid-svg-otHb88opK93k3lCq .icon-shape,#mermaid-svg-otHb88opK93k3lCq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-otHb88opK93k3lCq .icon-shape p,#mermaid-svg-otHb88opK93k3lCq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-otHb88opK93k3lCq .icon-shape .label rect,#mermaid-svg-otHb88opK93k3lCq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-otHb88opK93k3lCq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-otHb88opK93k3lCq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-otHb88opK93k3lCq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} object(所有类型的基类)
System.ValueType
引用类型
简单类型:int / long / double / bool / char / decimal...
struct 结构体(DateTime、自定义)
enum 枚举
Nullable<T> 可空值类型(int?)
string 字符串(不可变)
class / record class
interface 接口
数组 T\[\]
delegate 委托 / event

说明:System.Int32 本质就是一个 struct,所以所有数值类型天然都是值类型。

1.4 几个特殊成员

成员 类型 特殊之处
string 引用类型 不可变 ;== 被重载为按内容比较,用起来"像值类型"
record class 引用类型 自动按成员内容比较(值语义)
record struct 值类型 结构体 + 值语义比较
object / dynamic 引用类型 dynamic 编译期类型是 object,运行时再动态解析

2. 存储位置与赋值机制

2.1 破除常见误区

"值类型在栈上、引用类型在堆上"这句话并不准确,它只描述了局部变量的常见情况。

真实的规则是:

  • 值类型作为局部变量时,通常分配在栈上;
  • 值类型作为类的字段时,跟随对象存放在堆上;
  • 值类型作为数组元素时,存放在数组对象内部(堆上);
  • 值类型被装箱后,也存放在堆上。

所以栈/堆只是运行时的实现细节。值类型与引用类型真正的区别只有一条:赋值时复制的是数据本身,还是复制引用。

2.2 内存示意

值类型赋值(复制数据):

text 复制代码
栈(示意)
┌─────────────┐
│  a  =  10   │
├─────────────┤
│  b  =  10   │  ← b = a:拷贝整份数据
└─────────────┘
之后修改 b,a 不受影响

引用类型赋值(复制地址):

text 复制代码
栈(示意)              堆
┌─────────────┐      ┌──────────────┐
│  p  → 地址  ├─────→│  { 1, 2, 3 } │
├─────────────┤      │  (只有一份) │
│  q  → 地址  ├─────→└──────────────┘
└─────────────┘
p、q 指向同一个对象,改一个全变

2.3 代码案例

csharp 复制代码
// 值类型:拷贝的是数据
int x = 10;
int y = x;
y = 99;
Console.WriteLine($"x={x}, y={y}");        // x=10, y=99

// 引用类型:拷贝的是引用
var list1 = new List<int> { 1, 2, 3 };
var list2 = list1;
list2.Add(4);
Console.WriteLine($"list1.Count={list1.Count}");   // 4
Console.WriteLine(ReferenceEquals(list1, list2));  // True(同一个对象)

3. 参数传递

C# 默认所有参数都按值传递。但对两类类型,含义完全不同:

传参类型 方法内"修改对象内容" 方法内"给参数重新赋值"
值类型 不影响外部 不影响外部
引用类型 影响外部(改的是同一个堆对象) 不影响外部(只改了形参里的地址副本)

3.1 引用类型参数:改内容 vs 重新赋值

csharp 复制代码
static void TryChange(List<int> list)
{
    list.Add(100);               // ① 修改对象内容 → 外部可见
    list = new List<int>();      // ② 重新赋值     → 外部不可见
}

var list = new List<int>();
TryChange(list);
Console.WriteLine(list.Count);   // 输出 1(只有 ① 生效)

这是最常见的坑:以为 list = new ... 能把外部变量换成新对象,其实只改了形参里那份地址副本。

3.2 用 ref / out / in 真正按引用传递

csharp 复制代码
static void RealChange(ref List<int> list)
{
    list = new List<int> { 7, 8 };
}

var list2 = new List<int>();
RealChange(ref list2);
Console.WriteLine(string.Join(", ", list2));   // 输出 7, 8

对值类型同样适用:

csharp 复制代码
static void AddOne(int n)
{
    n++;          // 只修改副本
}

static void AddOneRef(ref int n)
{
    n++;
}

int num = 5;
AddOne(num);
Console.WriteLine(num);          // 输出 5(值类型按值传,外部不变)

AddOneRef(ref num);
Console.WriteLine(num);          // 输出 6(ref 按引用传,外部改变)

out 与 ref 类似,但 out 参数在方法内必须被赋值;in 用于只读地按引用传递,避免大结构体拷贝。


4. 相等性比较

  • 值类型 :== 默认比较内容(结构是否相等)。
  • 引用类型 :== 默认比较引用(是不是同一个对象)。
  • string 例外 :== 按内容比较。
  • ReferenceEquals 永远比较引用,与是否重写无关。
csharp 复制代码
using System.Text;

int x = 5, y = 5;
Console.WriteLine(x == y);                   // True(值比较)

var s1 = new StringBuilder("a");
var s2 = new StringBuilder("a");
Console.WriteLine(s1 == s2);                 // False(两个不同的对象)
Console.WriteLine(ReferenceEquals(s1, s2));  // False

string t1 = "abc", t2 = "abc";
Console.WriteLine(t1 == t2);                 // True(string 按内容比较)

string 有字符串常量池(驻留池) ,相同字面量会复用;StringBuilder 没有常量池,每次 new 都是全新对象。

以字符串拼接为例,理解 string 的"不可变":

csharp 复制代码
string str = "a";
str += "b";

它看起来像把 "b" 追加到了后面,真实过程是:

  1. 堆上原有的 "a" 保持不动;
  2. 在堆里新建一块内存 ,生成 "ab";
  3. str 的引用指向这个新对象;
  4. 旧的 "a" 变成垃圾,等待 GC 回收。

而 StringBuilder 不会反复新建对象 ,它直接在内部的字符缓冲区上追加内容、复用同一块内存,适合频繁拼接、动态组装长文本(日志、报文、SQL、串口/视觉输出字符串等)。

最后是浮点数比较:

csharp 复制代码
double d1 = 0.1 + 0.2;
double d2 = 0.3;
Console.WriteLine(d1 == d2);                       // False(浮点精度问题)

Console.WriteLine(Math.Abs(d1 - d2) < 1e-9);       // True(推荐:差值容差判断)
// 注意:float / double 比较有精度问题,建议用差值容差,不要直接用 ==

5. 装箱与拆箱

值类型转换成 object 或接口时,会在堆上创建一个包装对象,称为装箱 ;反向转换称为拆箱。

csharp 复制代码
int i = 42;
object o = i;        // 装箱:堆上新建包装对象
int j = (int)o;      // 拆箱:要求类型精确匹配,否则抛 InvalidCastException
Console.WriteLine(j);   // 42

性能注意:

  • 装箱有内存分配和数据拷贝的开销,高频场景应尽量避免;
  • 优先使用泛型集合 List<T> / Dictionary<TKey, TValue>,从根本上避免装箱。
csharp 复制代码
// 避免装箱:使用泛型集合
var list = new List<int> { 1, 2, 3 };

6. 常见陷阱

陷阱 1:struct 改不动集合元素

list[i] 返回的是一个副本,直接修改它会编译报错:

csharp 复制代码
struct Point
{
    public int X;
    public int Y;
}

var points = new List<Point>();
points.Add(new Point { X = 1, Y = 2 });

// points[0].X = 5;   // 编译错误:不能修改"索引器返回的临时变量"

Point p = points[0];   // 先取出副本
p.X = 5;               // 修改副本
points[0] = p;         // 再放回集合

Console.WriteLine(points[0].X);   // 输出 5

原因:struct 是值类型,存在 List 里时,list[下标] 取出来的是一份拷贝出来的复制品,而不是盒子里的原件。

陷阱 2:引用类型参数重新赋值无效

见第 3.1 节:修改对象内容生效,替换对象不生效;要在方法内替换外部对象,必须使用 ref。

陷阱 3:引用类型默认值为 null

csharp 复制代码
int n = default;        // 0
bool b = default;       // false
string s = default;     // null ------ 使用前必须判空

// int x = null;        // 编译错误:值类型不能为 null(除非可空)
int? y = null;          // 可以:可空值类型

引用类型不判空就直接使用,会抛出 NullReferenceException。

陷阱 4:浅拷贝 vs 深拷贝

引用类型默认的拷贝(MemberwiseClone)是浅拷贝 :克隆出的新对象仍然共享内部的引用成员。

csharp 复制代码
class Order
{
    public List<string> Items { get; set; } = new();
}

var order1 = new Order();
var order2 = order1;         // 两个变量指向同一个对象(共享)

// 需要独立副本时,要自己做深拷贝(复制每个引用成员)
var order3 = new Order();
order3.Items = new List<string>(order1.Items);   // 新建列表,彼此独立

浅拷贝(MemberwiseClone)只创建新对象、内部引用成员仍共享;深拷贝把对象及其内部所有引用对象全部复制,两个实例完全独立。


7. 判断口诀

类、接口、数组、委托、字符串 → 引用类型;结构体、枚举、数字、布尔、字符 → 值类型。

记不清时可以用代码兜底:

csharp 复制代码
typeof(int).IsValueType;        // True  → 值类型
typeof(string).IsValueType;     // False → 引用类型
typeof(DateTime).IsValueType;   // True(DateTime 是 struct)

附录:速查表

维度 值类型 引用类型
典型成员 int、double、bool、char、decimal、struct、enum string、class、interface、数组、delegate
赋值语义 复制整份数据 复制引用(共享对象)
修改影响 互不影响 通过任一变量修改,都作用于同一对象
默认值 0 / false null
== 默认行为 比较内容 比较引用(string、record 例外)
能否为 null 默认不能 可以
典型存储 局部变量在栈;作字段/数组元素时在堆 引用在栈(或内联),对象在堆

相关推荐
m0_380743872 小时前
Qt中导航栏实现的详细指南
开发语言·c++
mhmh1233 小时前
Python 对象模型与数据模型:魔术方法、描述符与元类深度解析
开发语言·python
估值探索者3 小时前
【Python量化策略实战 #02】多因子合成mom和vol两因子打分合成
开发语言·c++·python·数据挖掘·c#
IT小白杨3 小时前
市场调研的自动化访问,怎样控制节奏才不被误判?
运维·经验分享·安全·自动化·指纹浏览器
m0_380743874 小时前
PHP7.3和7.2字符串功能有什么不同
开发语言·php
m0_380743874 小时前
PHP 8.4的新语法怎么用才规范
开发语言·php
阿狗童鞋4 小时前
Java并发编程实战指南
java·开发语言
AOI小白新手上路4 小时前
从 Keil 调试视角看 51 指针,到 Cortex-M 内存映射访问寄存器 · 学习笔记
笔记·学习
phltxy5 小时前
C 语言自定义类型:结构体、位段、枚举与联合体
c语言·开发语言