使用Linq Where子句与ToList()时内存泄漏的原因探究
问题场景
在WPF应用的DataGrid相关代码中,最初用Linq实现的GetColumns方法会导致内存泄漏:关闭包含DataGrid的对话框后,实例无法被GC回收。通过JetBrains dotMemory分析发现,Func<IColumn, bool>委托作为GC根持有DataGrid实例。改成foreach循环实现后,泄漏消失,实例可正常回收。
Linq版本代码:
List<IColumn> GetColumns() { return _columnItems .Where(column => column is TextColumn<T> textColumn && textColumn.IsColumnEnabled ) .ToList(); }
foreach版本代码:
List<IColumn> GetColumns() { var columnsToReturn = new List<IColumn>(); foreach (var column in _columnItems) { if (column is TextColumn<T> textColumn && textColumn.IsColumnEnabled) columnsToReturn.Add(textColumn); } return columnsToReturn ; }
原因分析
Linq Where的迭代器对象残留引用
Linq的Where方法会返回一个WhereEnumerableIterator<T>类型的对象,这个对象会持有你传入的lambda表达式生成的Func<IColumn, bool>委托。虽然ToList()会立即枚举这个迭代器并生成List,但如果这个迭代器对象被意外保留(比如绑定到UI的某个未清理属性、事件订阅上下文,或是被长期存在的缓存容器引用),它就会一直存活。闭包捕获形成的引用链
你的lambda表达式是在类的实例方法中定义的,它会隐式捕获当前类的实例(this)——因为你访问了实例字段_columnItems,或是泛型参数T属于当前类。这导致Func委托持有了包含DataGrid的对话框实例的引用。当迭代器对象保留这个Func时,就形成了GC根 → Where迭代器 → Func → 对话框实例 → DataGrid的引用链,直接阻止GC回收相关实例。foreach版本无额外引用负担
foreach循环的实现是直接创建List并逐个判断添加元素,没有生成任何持有委托的中间迭代器对象,也没有闭包捕获带来的额外引用。方法执行完毕后,除了返回的List,没有其他残留对象持有对话框实例的引用,所以关闭对话框后,相关实例能被正常回收。
内容的提问来源于stack exchange,提问作者Cod Fish

