IEnumerable是只读接口为何有时可修改集合元素?
IEnumerable修改元素属性生效逻辑差异问题
问题描述
关于IEnumerable集合的读写特性存在较多讨论,核心困惑点为:IEnumerable集合有时表现为只读,有时却支持修改元素。
在.NET Core 3.1项目中曾遇到如下场景:需要在foreach循环中修改集合元素,但迭代完成后集合完全没有发生改动,最初判断该结果符合预期。
public async Task DoFoo(IEnumerable<SomeClass> data, CancellationToken cancellationToken) { foreach (var item in data) { item.Id = item.SomeOtherValue; // 已确认Id属性定义为{get;set;} } await SaveData(data, cancellationToken); }
上述代码执行后元素的Id属性仍为null,将参数强转为List后再修改就解决了该问题。
但在单独编写测试代码验证时,却发现集合元素确实被修改了,和之前遇到的现象完全矛盾,测试代码如下:
using System; using System.Diagnostics; using System.Collections.Generic; public class Program { public static void Main() { var iePerson = new[] { new Person(){Id = 2, Name="SomeName", Other=5} }; // 初始Other值为5 IEnumerable<Person> pien = iePerson; // 显式转为IEnumerable类型 DoFoo(pien); } private static void DoFoo(IEnumerable<Person> entities) { foreach(Person p in entities) { Console.WriteLine(p.Id); p.Id = p.Other; } foreach(Person p in entities) Console.WriteLine(p.Id); // 输出结果: // 2 // 5 <--- 预期输出2,但实际输出了修改后的5 } public class Person { public string Name {get;set;} public int Id {get;set;} public int Other {get;set;} } }
原因解释
IEnumerable本身只是定义了迭代能力的接口,从来没有「只读」的强制约束,两种现象的差异完全来自传入实例的底层实现区别:
- 若传入的
IEnumerable是已物化的内存集合(数组、List<T>、HashSet<T>等):迭代时返回的是集合中存储的原始引用类型对象引用,直接修改对象属性会作用于内存中的原有实例,只要集合本身没有被重建,后续任意次迭代拿到的都是修改后的对象,修改自然生效。上述测试用例传入的是数组实例,就属于这类场景,因此修改可以被保留。 - 若传入的
IEnumerable是延迟执行的迭代器(未物化的EF Core查询结果、LINQ链式方法返回的迭代器、自定义yield return方法返回值等):每次触发迭代都会重新执行迭代逻辑,生成全新的对象实例。第一次foreach循环中修改的只是迭代过程中临时生成的对象,循环结束后临时对象会被垃圾回收,后续迭代(包括SaveData方法内部的遍历)会重新执行迭代逻辑生成新对象,之前的修改完全不会被保留,就会出现「修改不生效」的现象。
.NET Core 3.1项目中遇到的修改失效问题,传入的data大概率是未提前调用ToList()/ToArray()物化的EF Core查询结果,或是其他延迟执行的LINQ迭代器。将参数强转为List的操作本质是提前触发一次全量迭代,把所有结果加载到内存生成固定的List集合实例,后续遍历修改的都是List中存储的固定对象引用,修改即可正常生效。
补充说明:所有标准集合的迭代器都禁止在
foreach遍历过程中对集合本身执行增删元素操作(会触发迭代器版本校验抛出异常),但修改集合内引用类型元素的属性不属于对集合本身的修改,不受该规则限制。
内容的提问来源于stack exchange,提问作者macm
相关产品推荐
相关产品推荐

