面试题答案
一键面试匿名方法和Lambda表达式里变量的作用域与生命周期特殊之处
-
作用域
- 与普通方法类似:匿名方法和Lambda表达式内部可以访问外部作用域的变量。例如:
int outerVariable = 10; Action action = () => Console.WriteLine(outerVariable);
这里的Lambda表达式可以访问外部的
outerVariable
。其作用域从声明处开始,到包含该匿名方法或Lambda表达式的代码块结束。- 捕获变量:匿名方法和Lambda表达式会捕获它们使用的外部变量。即使外部变量在其声明的块结束后“应该”超出作用域,只要匿名方法或Lambda表达式还存在,捕获的变量就会一直存在。例如:
List<Action> actions = new List<Action>(); for (int i = 0; i < 3; i++) { actions.Add(() => Console.WriteLine(i)); } foreach (var action in actions) { action(); }
这里每个Lambda表达式都捕获了
i
。在这段代码中,i
的作用域是整个for
循环块,而每个Lambda表达式捕获的是同一个i
变量。 -
生命周期
- 与闭包相关:由于捕获变量,匿名方法和Lambda表达式中捕获的变量的生命周期会延长到包含它们的委托对象不再被使用为止。比如上述
actions
列表中的委托对象存在,i
变量就会一直存在,不会随着for
循环结束而销毁。
- 与闭包相关:由于捕获变量,匿名方法和Lambda表达式中捕获的变量的生命周期会延长到包含它们的委托对象不再被使用为止。比如上述
闭包概念分析
闭包是指一个函数(在这里是匿名方法或Lambda表达式)和其周围状态(捕获的变量)的组合。在C#中,匿名方法和Lambda表达式通过捕获外部变量形成闭包。例如上面for
循环中创建的Lambda表达式和捕获的i
变量就构成了闭包。闭包使得这些表达式可以在不同的时间和上下文中执行,并且保持对捕获变量的访问。
实际应用中可能遇到的问题
-
循环变量捕获问题
- 如上述
for
循环捕获i
的例子,预期输出可能是0 1 2
,但实际输出是3 3 3
。这是因为所有Lambda表达式捕获的是同一个i
变量,当for
循环结束后,i
的值变为3,所以每次执行Lambda表达式时输出的都是3。
- 如上述
-
内存泄漏风险
- 如果在匿名方法或Lambda表达式中捕获了大对象,并且这些委托对象长时间存活(例如被缓存起来),那么捕获的大对象也会长时间存活,可能导致内存泄漏。比如捕获了一个非常大的
byte[]
数组,而包含捕获该数组的委托对象一直被某个静态集合持有。
- 如果在匿名方法或Lambda表达式中捕获了大对象,并且这些委托对象长时间存活(例如被缓存起来),那么捕获的大对象也会长时间存活,可能导致内存泄漏。比如捕获了一个非常大的
解决方案
- 解决循环变量捕获问题
- 使用临时变量:在
for
循环内部创建一个临时变量,每个Lambda表达式捕获不同的临时变量。例如:
这样每个Lambda表达式捕获的是不同的List<Action> actions = new List<Action>(); for (int i = 0; i < 3; i++) { int temp = i; actions.Add(() => Console.WriteLine(temp)); } foreach (var action in actions) { action(); }
temp
变量,输出结果就是0 1 2
。- 使用
foreach
:foreach
循环在每次迭代时会创建一个新的迭代变量,所以可以避免上述问题。例如:
List<int> numbers = new List<int>() { 0, 1, 2 }; List<Action> actions = new List<Action>(); foreach (var number in numbers) { actions.Add(() => Console.WriteLine(number)); } foreach (var action in actions) { action(); }
- 使用临时变量:在
- 避免内存泄漏
- 及时释放委托对象:如果委托对象不再需要,及时将其设置为
null
,让垃圾回收器可以回收相关资源。例如,如果将委托对象存储在一个集合中,当不再需要这些委托对象时,清空集合,使引用断开,从而让捕获的变量可以被垃圾回收。 - 避免捕获不必要的大对象:在匿名方法或Lambda表达式中尽量只捕获必要的变量,避免捕获大对象。如果必须捕获大对象,可以考虑在合适的时机手动释放大对象的资源,比如在委托执行完毕后将大对象设置为
null
。
- 及时释放委托对象:如果委托对象不再需要,及时将其设置为