在循环内修改元素会扰乱 for 循环

Modifying element inside a loop messes with the for loop

我正在使用以下 for 循环遍历 IEnumerable:

for (int i = 0; i < items.Count(); i++)
{
    if (cancellationToken.IsCancellationRequested)
    {
        return;
    }

    var obj = items.ElementAt(i);

    obj.TranslatedText = await Task.Run(() => Translator.Translate(obj.EnglishText, "English", File.Lang));

    progress.Report(i + 1);

    await Task.Delay(DELAY);
}

以上代码跳过了备用元素。即使计数为 7,循环也只有 4 次 运行。

我试图用等效的 foreach 循环替换 for 循环:

int current = 0;
foreach (var item in items)
{
    if (cancellationToken.IsCancellationRequested)
    {
        return;
    }

    item.TranslatedText = await Task.Run(() => Translator.Translate(item.EnglishText, "English", File.Lang));

    progress.Report(++current);

    await Task.Delay(DELAY);
}

它工作正常,我无法找出两者之间的区别。

我仔细研究了一下,发现如果删除该行

obj.TranslatedText = await Task.Run(() => Translator.Translate(obj.EnglishText, "English", File.Lang));

从第一个示例来看,它执行得很好。

我不能修改 IEnumerable 的内容吗?只是好奇。

更新 1 我在下面发布了一个可重现的示例。

https://pastebin.com/5ZXky7iX

现在我们有了一个完整的例子,大家可以看看问题出在哪里了。您用于 items 的集合是一个依赖于 TranslatedText:

的查询
var source = collection.Where(x => string.IsNullOrEmpty(x.TranslatedText));

您对 obj 采取的操作会使该查询的结果无效,对于您正在处理的项目:

obj.TranslatedText = "something";

所以在你的 for 循环中,最初所有 10 个 Translation 对象都满足条件,所以 Count() 是 10。在循环的第一次迭代中,你访问第一个元素(元素 0),并将 obj.TranslatedText 设置为 "something".

现在,在循环的 次迭代中,您正在计算“查询的当前结果” - 现在是 9。然后您将按索引访问元素在查询的当前结果中 - 所以当 i 为 1 时,您将跳过查询的第一个匹配项并访问第二个匹配项。但这不是原始集合中的第二个匹配项 - 它是当前查询 的第二个匹配项 ,它已经跳过了第一个元素,因为您修改了它以设置翻译。因此,原始元素索引 1 在循环的第二次迭代中被跳过,您改为设置原始元素索引 2 的翻译文本。然后Count()变成8,等等

使用 foreach 循环,您只对查询进行一次迭代 - 当您仍然使“您正在查看的当前元素”的查询条件无效时,查询处理不会无论如何都需要再次检查。

所以要么使用 foreach 循环,要么如果你想通过索引访问元素,你应该先 materialize 查询。例如,您可以使用:

// Evaluate the query once, storing the results in a list
var list = items.ToList();
// Now you can operate on the list without worrying about the query
// being reevaluated.
for (int i = 0; i < list.Count; i++)
{
    if (cancellationToken.IsCancellationRequested)
    {
        return;
    }
   var obj = list[i];
   obj.TranslatedText = await Task.Run(() => Translator.Translate(obj.EnglishText, "English", File.Lang));
   progress.Report(i + 1);
   await Task.Delay(DELAY);
}