由于 .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&lt;Guid&gt; 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 属性设置器中放置一个断点,看看在IEnumerableList 的情况下该值设置了多少次

【讨论】:

【参考方案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&lt;Foo&gt;。扩展方法对 GetEnumerator 的每次调用都会获得相同的稳定数据,因为 List 已经有了它的元素值。

【讨论】:

以上是关于由于 .net-core 中的 linq-statement 导致 IEnumerable 和 List 之间出现意外差异? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

获取 ASP.NET-Core 2.2 控制器中的控制器名称和方法名称

从类库访问 Asp.net-core 中的 appsetting.json

将字符串数组发布到 .net-core mvc

已发布的 .Net-Core 应用程序无法运行

Linq 查询在 ASP.NET-Core 3.0 及更高版本中对数字等字符串进行排序

SqlException:INSERT 语句与 FOREIGN KEY 约束冲突 - asp.net-core