一、作用域(Scope)
作用域 = 变量能被访问的代码范围
简单说:变量在哪里 "看得见",哪里就是它的作用域。
C# 常见作用域:
-
局部作用域 :方法内、代码块内(
if/for/while/using) -
类作用域:字段(成员变量),整个类都能访问
-
命名空间 / 全局作用域 :极少用
csclass Test { // 类作用域(字段) private int classVar = 10; void Method() { // 方法局部作用域 int localVar = 20; if (true) { // 代码块局部作用域 int blockVar = 30; } // 错误:blockVar 超出作用域,看不见 // blockVar = 100; } }规则:
-
内层作用域可以访问外层变量
-
外层作用域不能访问内层变量
-
作用域不直接决定内存位置,但影响生命周期
二、生命周期(Lifetime)
生命周期 = 变量从创建到销毁的时间
1. 局部变量(值类型 / 栈上引用)
-
生命周期:进入代码块 → 离开代码块
-
存储:栈(Stack)
-
销毁:自动释放,无需 GC
csvoid Test() { int a = 10; // 出生 // 使用 } // a 死亡,栈内存自动释放2. 局部变量(引用类型,new 对象)
-
对象本身在堆(Heap)
-
引用变量在栈
-
生命周期:
-
引用变量:方法结束就销毁
-
堆上对象:直到没有引用指向它,被 GC 回收
csvoid Test() { var obj = new MyClass(); // 引用(栈)+ 对象(堆)出生 } // 引用变量obj销毁 // 堆上对象:等待GC回收3. 类字段(成员变量)
-
生命周期:和所属对象一致
-
对象创建 → 字段出生
4. 静态字段
-
生命周期:程序运行期间永远存在
-
存储在高频堆(High Frequency Heap)
-
只有程序退出才会释放
-
三、变量逃逸(Variable Escaping)
什么是逃逸?
变量本来应该在栈上,但是因为被 "外部引用",被迫跑到堆上,无法在栈上自动释放 → 这就叫逃逸。
逃逸 = 局部变量逃离了它的局部作用域,生命周期被延长了。
为什么要关心逃逸?
- 栈:极快、自动释放、无 GC 开销
- 堆:较慢、需要 GC、产生内存压力
- 大量逃逸 = 更多 GC = 性能下降
四、C# 中 4 种最常见的逃逸场景
场景 1:局部对象被方法返回
cs
MyClass CreateObject()
{
var obj = new MyClass(); // 局部变量
return obj; // 对象被返回 → 逃逸到堆
}
obj引用在栈上,但堆上对象被返回,脱离了方法作用域- 对象逃逸
场景 2:局部变量被委托 / Lambda 捕获
cs
Action Test()
{
int num = 10; // 局部值类型
return () => Console.WriteLine(num); // 捕获num
}
- num 本应在栈
- 被 Lambda 捕获 → 编译器生成闭包类 → num 跑到堆
- 值类型发生了 "装箱 / 闭包逃逸"
场景 3:局部变量被类字段引用
cs
class Test
{
MyClass _field;
void Method()
{
var local = new MyClass();
_field = local; // 局部变量赋值给类字段
}
}
local指向的对象被类成员持有- 生命周期延长 → 逃逸
场景 4:在迭代器(yield)/async 方法中
cs
IEnumerable<int> Test()
{
int num = 10;
yield return num;
}
五、逃逸后会发生什么?(底层机制)
C# 编译器会做两件事:
- 闭包类:把捕获的变量变成类的字段
- 装箱:值类型变成引用类型(存到堆)
结果:
- 栈变量 → 堆变量
- 生命周期从 "方法结束即销毁" → 变成 GC 管理
- 产生GC 开销
六、生命周期 + 作用域 + 逃逸
| 变量类型 | 作用域 | 生命周期 | 是否逃逸 | 存储位置 |
|---|---|---|---|---|
| 普通局部值类型 | 方法 / 代码块 | 代码块内 | 不逃逸 | 栈 |
| 局部引用类型(不返回) | 方法内 | 方法内 | 不逃逸 | 引用 = 栈,对象 = 堆 |
| 局部引用类型(被返回) | 方法内 | 延长到外部 | 逃逸 | 堆 |
| 被 Lambda 捕获的局部变量 | 方法内 | 延长到委托生命周期 | 逃逸 | 堆(闭包类) |
| 类字段 | 整个类 | 和对象同生灭 | 不逃逸(天然堆) | 堆 |
| 静态字段 | 全局 | 程序全程 | 不逃逸 | 高频堆 |
七、如何写出 "不逃逸" 的高性能代码?
- 局部变量尽量不要返回引用类型
- Lambda 尽量少捕获外部变量(尤其是循环中)
- 能用值类型就不用引用类型
- 迭代器(yield)/async 少用局部大对象
- (C# 7.2+)用
Span<T>/stackalloc强制栈分配,完全避免逃逸
总结
- 作用域:变量能被访问的代码范围
- 生命周期:变量在内存中存活的时间
- 变量逃逸:局部变量被迫从栈跑到堆,生命周期被延长
- 逃逸危害:增加 GC 压力,降低性能
- 核心优化 :尽量让局部变量不逃逸,留在栈上