由于 .net-core 中的 linq-statement 导致 IEnumerable 和 List 之间出现意外差异? [复制]
Posted
技术标签:
【中文标题】由于 .net-core 中的 linq-statement 导致 IEnumerable 和 List 之间出现意外差异? [复制]【英文标题】:Unexpected difference between IEnumerable and List as result of a linq-statement in .net-core? [duplicate] 【发布时间】:2020-01-31 21:36:28 【问题描述】:当我在 Select() 语句的末尾添加 .ToList() 时,它按预期工作。 这种情况下 IEnumerable 和 List 有什么区别?
class Foo
public string Name get; set;
public Guid Id get; set;
class Program
static void Main(string[] args)
var list = new List<string>() "first", "last" ;
var bar = list.Select(x => new Foo
Name = x,
Id = Guid.NewGuid()
); //.ToList();
Console.WriteLine("bar Count = " + bar.Count());
Console.WriteLine();
for (int i = 0; i < 3; i++)
Console.WriteLine(bar.First().Name + " " + bar.First().Id);
Console.WriteLine(bar.Last().Name + " " + bar.Last().Id);
Console.WriteLine();
结果:
示例数 = 2
第一个 1fabd003-340c-44a4-81ca-1908f206ccdb 最后 213f40c9-0705-4676-ad1f-73de21c5bc62
第一个 9bc117e5-1f10-4855-af95-46258b39955c 最后 cd787227-12d9-4e25-88bc-634369d85671
第一个 c787e081-8dd3-475c-bf24-44e5f103ed3a 最后 7a72080d-2dee-47b1-a55d-af0a7665d0ea
.ToList() 的结果
柱数 = 2
第一个 3c1d4f1d-eecd-438a-86a4-e7db0680cad3 最后一个 cba1867d-d246-477e-b938-0f9cce457a5c
第一个 3c1d4f1d-eecd-438a-86a4-e7db0680cad3 最后一个 cba1867d-d246-477e-b938-0f9cce457a5c
第一个 3c1d4f1d-eecd-438a-86a4-e7db0680cad3 最后一个 cba1867d-d246-477e-b938-0f9cce457a5c
【问题讨论】:
这个问题并不完全清楚,但我猜这与Select
LINQ 查询推迟操作直到枚举的事实有关。当您添加 ToList
时,您正在枚举它
看来你混淆了IEnumerable
和收藏。 IEnumerable
并不是真正的集合,而更像是集合的草稿,它包含IEnumerator
- 知道 如何枚举集合的对象。只有当我们执行一个需要枚举它的函数时,它才会真正执行任何东西,.ToList()
枚举它并返回一个集合,而.First
执行它并返回第一个元素
我已经说了十多年了,而且似乎每天都需要重复。 查询对象代表一个问题,而不是一个答案。你可以问同样的问题两次,得到不同的答案!在您的原始代码中,您要问这个问题三遍。在您修改后的代码中,您只询问一次并缓存结果。
类似地,我们可以说Func<Guid> f = Guid.NewGuid;
,我们会发现Console.WriteLine($"f()f()f()");
和g = f(); Console.WriteLine($"ggg");
有不同的行为。这种情况是否让您感到惊讶,或者这会是您所期望的差异?
【参考方案1】:
当您使用Select
时,您正在定义一个惰性枚举,这意味着您正在定义有关如何生成可枚举中的值的规则,但尚未实际生成值本身。惰性可枚举(也称为“延迟”)是蓝图,仅此而已 - 它们不包含数据并且不执行任何代码。
接下来,当您调用bar.First()
和bar.Last()
时,您将触发可枚举以开始生成一些值。 每次执行此操作时,它都会生成全新的值。 这意味着每次在 bar
上调用某些东西时,它都会通过对 Guid.NewGuid()
的新调用来生成新值,这自然会导致每次都生成新的 GUID。
但是,如果您在可枚举对象上调用 ToList()
,您将在可枚举对象中生成值,然后将它们缓存在 List
中。现在当您调用bar.First()
和bar.Last()
时,您指的是List
中第一个和最后一个元素的静态值,它们不会改变。
【讨论】:
【参考方案2】:System.Linq
使用延迟执行。当您使用IEnumerable
时,每次都会生成一个新的Foo
值,当您在循环中迭代可枚举时,每个实例都会使用新的Id
值。当您调用First()
或Last()
时,您会得到一个具有不同Guid
值的新实例。
当使用ToList()
时,Linq
表达式会立即计算,Foo
实例在集合中保持不变,Id
值已经计算。您的循环使用相同的值迭代集合,First()
和 Last()
在每次 for
循环迭代时返回相同的实例。因此,您每次都会得到相同的 Guid
值。
您可以在Id
属性设置器中放置一个断点,看看在IEnumerable
和List
的情况下该值设置了多少次
【讨论】:
【参考方案3】:您在IEnumerable
中创建了对象,每次枚举它时,它都会创建新项目。您对First()
和Last()
的调用可能会生成两个不同的迭代,生成不同的内容。
如果您之前使用ToList
存储它,则每次迭代的内容都保持不变。
【讨论】:
【参考方案4】:var bar = list.Select(x => new Foo
Name = x,
Id = Guid.NewGuid()
);
每次调用bar
值的GetEnumerator 方法时,它都会生成一个枚举器,该枚举器为list
中的每个元素执行函数。由于中间有一个 Guid.NewGuid(),它是不稳定的。
Console.WriteLine("bar Count = " + bar.Count());
Console.WriteLine(bar.First().Name + " " + bar.First().Id);
Console.WriteLine(bar.Last().Name + " " + bar.Last().Id);
这些行中的每一行都调用一个调用GetEnumerator()
的扩展方法,这导致生成了5 个不同的GUID(First()
是智能的,并且在第一个之后停止)。
var bar = list.Select(x => new Foo
Name = x,
Id = Guid.NewGuid()
).ToList();
这会根据运行 SelectEnumerable 一次的结果生成 List<Foo>
。扩展方法对 GetEnumerator 的每次调用都会获得相同的稳定数据,因为 List 已经有了它的元素值。
【讨论】:
以上是关于由于 .net-core 中的 linq-statement 导致 IEnumerable 和 List 之间出现意外差异? [复制]的主要内容,如果未能解决你的问题,请参考以下文章
获取 ASP.NET-Core 2.2 控制器中的控制器名称和方法名称
从类库访问 Asp.net-core 中的 appsetting.json