C# 为什么LINQ是非确定性的?

C# 为什么LINQ是非确定性的?,c#,linq,ienumerable,deferred-execution,non-deterministic,C#,Linq,Ienumerable,Deferred Execution,Non Deterministic,我对一个IEnumerable进行了随机排序。我不断打印出相同的元素,得到不同的结果 string[] collection = {"Zero", "One", "Two", "Three", "Four"}; var random = new Random(); var enumerableCollection = collection.OrderBy(e => random.NextDouble()); Console.WriteLine(enumerableCollection.E

我对一个
IEnumerable
进行了随机排序。我不断打印出相同的元素,得到不同的结果

string[] collection = {"Zero", "One", "Two", "Three", "Four"};
var random = new Random();
var enumerableCollection = collection.OrderBy(e => random.NextDouble());

Console.WriteLine(enumerableCollection.ElementAt(0));
Console.WriteLine(enumerableCollection.ElementAt(0));
Console.WriteLine(enumerableCollection.ElementAt(0));
Console.WriteLine(enumerableCollection.ElementAt(0));
Console.WriteLine(enumerableCollection.ElementAt(0));
每次写入都会产生不同的随机元素。为何不维持秩序


前面的许多回答在技术上都是正确的,但我认为直接查看应用程序的实现是有用的

我们可以看到这些调用,而这些调用又会在当前分组的迭代中产生

如果您不熟悉Yeld,请参阅

对于给定的示例,当前分组为

e => random.NextDouble()
这意味着我们在集合上无休止地迭代(查看yield的inifinite循环),并返回针对random.NextDouble()执行的每个元素的分组分辨率


因此,这里的不确定性是由于随机分组的不确定性。这是LINQ terminal语句(.List()、toArray()等)的预期行为,但如果事先尝试使用它们,则可能会造成混乱。

LINQ会将执行延迟到绝对必要时。您可以将
enumerableCollection
视为希望枚举如何工作的定义,而不是单个枚举的结果

因此,每次您枚举它时(您在调用
ElementAt
时正在执行此操作),它都会重新枚举您的原始集合,并且由于您选择随机排序,每次的答案都不同

通过在末尾添加
.ToList()
,您可以使其达到预期效果:

var enumerableCollection = collection.OrderBy(e => random.NextDouble()).ToList();

这将执行枚举,并将结果存储在列表中。通过使用此选项,不会在每次枚举
enumerableCollection

时重新枚举原始列表。简单的答案是,您具体地强制了一种不确定的情况。原因在于LINQ的工作方式

LINQ的核心是围绕构建表示一组数据上的操作的对象(对象、记录等)的概念而设计的,然后在枚举时执行这些操作。调用
OrderBy
时,返回的是一个对象,它将处理原始数据以执行排序,而不是一个已排序的新集合。只有当您实际枚举数据时,才有顺序—在本例中,通过调用
ElementAt

每次枚举
IEnumerable
对象时,它都会运行指定的操作序列,创建元素的随机顺序。因此,每个
ElementAt(0)
调用将返回新随机序列的第一个元素

基本上,您似乎误解了这一点:
IEnumerable
不是集合

但是,您可以使用
ToArray()
ToList()
IEnumerable
转换为集合。它们将获取由
IEnumerable
指定的操作序列的输出,并分别创建一个新集合-
字符串[]
列表

例如:

string[] collection = {"Zero", "One", "Two", "Three", "Four"};
var random = new Random();
var enumerableCollection = collection.OrderBy(e => random.NextDouble()).ToArray();

Console.WriteLine(enumerableCollection.ElementAt(0));
Console.WriteLine(enumerableCollection.ElementAt(0));
Console.WriteLine(enumerableCollection.ElementAt(0));
Console.WriteLine(enumerableCollection.ElementAt(0));
Console.WriteLine(enumerableCollection.ElementAt(0));

现在,您将从原始数组中获得一个随机项,但它将多次成为同一项。它不是每次随机阅读,而是阅读顺序随机的新集合中的第一项。

我很高兴问题就在这里。虽然延迟执行的概念众所周知,但人们仍然会犯错误并多次枚举
IEnumerable
,即使Eric Lippert在年也没有避免这一点。这样做可能最终会破坏您的代码。如果需要多次枚举
IEnumerable
,请具体化它(例如使用
ToList

编辑

考虑到Eric Lippert的权威,让我来说明多重枚举有多糟糕。以下是基于列表的正确结果:

Zero,One,Two
Zero,One,Three
Zero,One,Four
Zero,Two,Three
Zero,Two,Four
Zero,Three,Four
One,Two,Three
One,Two,Four
One,Three,Four
Two,Three,Four
下面是我们处理顺序不稳定的序列时发生的情况:

Three,One,Four
Four,Two,One
One,Zero,Three
One,Zero,Four
Three,Two,Zero
One,Four,Zero
Two,Four,One
Two,Three,Four
Two,Three,Four
Four,Three,One
切中要害

  • 其中一个枚举是对序列进行计数;序列 在多次枚举时没有稳定计数的 您不应该尝试组合的序列
  • 计数不一定枚举序列
  • 在该系列文章中,我强烈鼓励使用不可变的数据结构,这些结构在枚举时总是安全且性能良好的 多次
  • 我同意,试图用不稳定计数组合序列是有问题的。但事实并非如此。序列已经给出了计数和保证项,它们只是随机排序的。想象一下,这可能是某种奇特的哈希集的结果,该哈希集可以出于优化原因在枚举之间重新排列其内部结构
  • 这是真的。这种可能性是存在的。但这真的是一个论点吗
  • 如果那是合同,好的。您希望序列可以根据需要多次枚举,并且总是得到相同的结果。明确地说可以避免混淆,并不是每个序列都有这个属性

  • 为什么每次都希望它返回相同的值?要理解的关键问题可能是LINQ查询在实现之前不会做任何事情(例如,
    ToList
    ToArray
    foreach
    ElementAt
    )。您没有执行一次查询然后获取第一个元素。你要把它具体化五次。所以每次,它的顺序都不同——就像你告诉它的那样。@Evorlor阅读:“此方法通过使用延迟执行来实现。立即返回值是存储执行操作所需的所有信息的对象。通过直接调用对象的GetEnumerator方法或使用foreach.枚举对象之前,不会执行此方法表示的查询。重用该
    IEnumerable
    (例如,通过使用
    .ElementAt(0)将其具体化