【一文了解】闭包
目录
闭包
1.C#和Unity中闭包的本质
1.1.闭包的定义
1.2.基本示例
1.3.闭包的底层原理
2.多线程+循环中闭包的典型问题
3.处理循环变量的核心技巧:“变量快照”
4.Unity中闭包的其他注意事项
4.1.闭包与协程的结合
4.2.闭包与内存泄漏
5.闭包使用技巧
本篇文章分享一下在C#和Unity中的闭包(Closure)。在C#和Unity中,闭包(Closure)是指匿名函数(或lambda表达式)捕获并访问其外部作用域变量的现象。这种特性在简化代码的同时,也可能引发意外行为,尤其是在多线程和循环结合的场景中。
闭包
1.C#和Unity中闭包的本质
1.1.闭包的定义
闭包是匿名函数(或lambda表达式)捕获并访问其外部作用域变量的现象。即使外部变量所在的作用域已销毁,匿名函数仍能访问和修改这些变量。
1.2.基本示例
private void Start()
{int count = 0;//外部变量//匿名函数捕获 count,形成闭包Action fun = () => {count++;Debug.Log($"count: {count}");};fun();//输出: count: 1fun();//输出: count: 2
}
匿名函数()=>{}捕获了外部变量count,即使Start方法执行结束,fun仍能修改count。
1.3.闭包的底层原理
C#编译器会为闭包创建一个匿名类,将捕获的变量作为该类的字段,匿名函数作为类的方法。例如,上面的代码会被编译为类似:
//编译器生成的匿名类
private sealed class AnonymousClass
{public int count; // 捕获的外部变量public void Fun(){count++;Debug.Log($"count: {count}");}
}// Start 方法实际执行逻辑
private void Start()
{AnonymousClass obj = new AnonymousClass();obj.count = 0;Action fun = obj.Fun;fun();fun();
}
2.多线程+循环中闭包的典型问题
在循环中创建多线程并使用闭包捕获循环变量时,容易出现变量值不符合预期的问题。这是因为闭包捕获的是变量本身,而非变量在循环某一轮的值。如Unity中多线程读取文件(错误示例):
private void Start()
{string[] filePaths = { "file1.txt", "file2.txt", "file3.txt" };foreach (var path in filePaths){//错误:直接捕获循环变量 pathnew Thread(() => {Debug.Log($"读取文件: {path}");//输出多个重复路径(如 3 次 "file3.txt")}).Start();}
}
问题原因:foreach循环的迭代变量path在编译时被视为循环体外定义的单一变量(所有迭代共享同一个变量)。线程启动后,循环可能已执行多轮,path的值已改变,导致多个线程读取到相同的最终值。
3.处理循环变量的核心技巧:“变量快照”
解决上述问题的关键是在每轮循环中创建临时变量,固化当前迭代的变量值,让闭包捕获临时变量而非循环变量。即“变量快照”,在特定时刻为变量创建一个“副本”,固化其当前值,避免后续代码对原变量的修改影响到依赖该变量的逻辑。简而言之,“变量快照”就是“给变量拍个照存起来,用的时候看照片,不管原变量后来变成啥样”。如Unity中多线程读取文件(正确示例:多线程+循环+闭包)
private void Start()
{string[] filePaths = { "file1.txt", "file2.txt", "file3.txt" };foreach (var path in filePaths){//变量快照:每轮循环创建临时变量,保存当前 path 的值string currentPath = path; new Thread(() => {Debug.Log($"读取文件: {currentPath}");//正确输出每个文件路径}).Start();}
}
原理:currentPath是循环每轮创建的新变量,值为当前迭代的path。闭包捕获的是currentPath,其值在本轮循环中固定,因此每个线程会使用正确的路径。
4.Unity中闭包的其他注意事项
4.1.闭包与协程的结合
在协程中使用闭包时,需注意捕获的变量可能被延迟修改:
using UnityEngine;
using System;
using System.Collections;public class ClosureTest1 : MonoBehaviour
{private IEnumerator Start(){int frame = 0;//协程中启动另一个协程,闭包捕获 frameStartCoroutine(MyCoroutine(() =>{Debug.Log($"当前帧: {frame}");//输出的是 frame 的最终值(而非启动时的值)}));//延迟修改 frameyield return new WaitForSeconds(1);frame = 100;}private IEnumerator MyCoroutine(Action callback){yield return new WaitForSeconds(2);//等待 2 秒,此时 frame 已被修改为 100callback();//输出: 当前帧: 100}
}
解决:同样使用变量快照:
using UnityEngine;
using System;
using System.Collections;public class ClosureTest1 : MonoBehaviour
{private IEnumerator Start(){int frame = 0;int currentFrame = frame;//固化当前值StartCoroutine(MyCoroutine(() =>{Debug.Log($"当前帧: {currentFrame}");}));yield return new WaitForSeconds(1);frame = 100;}private IEnumerator MyCoroutine(Action callback){yield return new WaitForSeconds(2);callback();//输出: 当前帧: 0}
}
4.2.闭包与内存泄漏
闭包会延长捕获变量的生命周期,若变量是Unity组件(如GameObject、MonoBehaviour),可能导致内存泄漏:
public class ClosureTest2 : MonoBehaviour
{private void Start(){GameObject obj = new GameObject("Temp");//闭包捕获 obj,即使 obj 被销毁,闭包仍持有引用Action action = () => Debug.Log(obj.name);Destroy(obj);//销毁对象,但闭包仍引用 obj,导致其无法被 GC 回收}
}
解决:避免在长生命周期的闭包中捕获Unity组件。必要时手动解除引用(如设为null)。
5.闭包使用技巧
(1)多线程+循环:每轮循环定义临时变量(如上述例子中的currentPath = path),让闭包捕获临时变量,避免共享循环变量导致的值混乱。
(2)变量快照:用临时变量固化捕获的值,确保闭包使用的是预期值而非延迟修改后的值。
(3)内存管理:避免闭包长期持有Unity组件引用,必要时手动释放,防止内存泄漏。
(4)调试技巧:若闭包行为异常,可将匿名函数改为显式方法,通过参数传递变量值,降低调试难度。
合理利用闭包能简化代码,但需注意其对变量生命周期的影响,尤其是在多线程和循环场景中。
好了,本次的分享到这里就结束啦,希望对你有所帮助~