2

所以我创建了一个我想删除的项目字典的投影。

var toRemoveList =
    this.outputDic.Keys.Where(key =>
        this.removeDic.ContainsKey(key));

然后我遍历从实际字典中删除的结果

        foreach(var key in toRemoveList)
            this.outputDic.Remove(key);

但是,在该 foreach 期间,会引发异常,说明该列表在循环期间已被修改。但是,怎么会呢?linq 查询是否有些动态,并且每次字典更改时都会重新评估?在查询结束时调用一个简单的 .ToArray() 可以解决问题,但在 imo 中,它甚至不应该首先发生。

4

5 回答 5

14

所以我创建了一个我想删除的项目字典的投影。

var toRemoveList =
  this.outputDic.Keys.Where(key =>
    this.removeDic.ContainsKey(key));

正如我经常说的,如果我可以教人们关于 LINQ 的一件事,那就是: 查询表达式的结果是查询,而不是执行查询的结果。你现在有一个对象,它的意思是“字典的键,使得键是……某物”。不是那个查询的结果,而是那个查询。查询本身就是一个对象;它不会给你一个结果集,直到你要求一个。

然后你这样做:

    foreach(var key in toRemoveList)
        this.outputDic.Remove(key);

那么你正在做什么?您正在遍历查询。遍历查询会执行查询,因此查询会遍历原始字典但是,当您迭代它时,您从字典中删除了一个项目,这是非法的。

imo,它甚至不应该首先发生。

你对世界应该是什么样子的看法很普遍,但按照你的方式去做会导致效率低下。让我们假设创建查询会立即执行查询,而不是创建查询对象。这是做什么的?

var query = expensiveRemoteDatabase
    .Where(somefilter)
    .Where(someotherfilter)
    .OrderBy(something);

第一次调用Where产生一个查询,然后在您的世界中执行该查询,从远程数据库中提取与该查询匹配的所有记录。然后第二个电话Where说“哦,对不起,我的意思是在这里也应用这个过滤器,我们可以再次执行整个查询,这次使用第二个过滤器吗?” 然后计算整个记录集,然后我们说“哦,不,等等,我忘了告诉你,当你构建最后一个查询对象时,我们需要对它进行排序,所以数据库,你能运行第三次问我这个问题?”

现在也许你明白为什么查询会产生一个查询,然后直到它需要才执行?

于 2013-08-02T17:45:48.400 回答
3

原因是它toRemoveList不包含要移除的事物列表,它包含了如何获取可移除事物列表的描述。

如果您在调试器中使用 F11 逐步执行此操作,您可以自己清楚地看到这一点。它停止的第一点foreach是您所期望的光标。

接下来,您停在toRemoveList(中的那个foreach(var key in toRemoveList))。这是它设置迭代器的地方。

但是,当您单步var key执行(使用 F11)时,它现在会跳转到 的原始定义toRemoveList,特别是该this.removeDic.ContainsKey(key)部分。现在您对真正发生的事情有所了解。

foreach正在调用迭代器方法Next以移动到字典键中的下一个点并保留列表。当您调用this.outputDic.Remove(key);this 时,检测到迭代器尚未完成,因此会因此错误而停止您。

正如每个人在这里所说的那样,解决这个问题的正确方法是使用ToArray()/ToList()因为它们的作用是给你另一个列表副本。因此,您有一个要逐步完成,一个要从中删除。

于 2013-08-02T17:37:33.187 回答
2

解决了这些.ToArray()问题,因为它迫使您评估整个枚举并缓存本地值。如果不这样做,当您枚举它时,可枚举尝试计算第一个索引,返回它,然后返回到集合并计算下一个索引。如果您正在迭代的基础集合发生更改,则您不能再保证枚举将返回适当的值。

简而言之:只需使用.ToArray()(或.ToList(),或其他)强制评估。

于 2013-08-02T17:29:35.430 回答
2

您收到此错误的原因是延迟执行 linq。要在循环运行时充分解释它实际上是从字典中获取数据时。因此,此时会在 outputdic 中进行修改,并且不允许修改您正在循环的集合。这就是您收到此错误的原因。您可以通过在运行循环之前要求编译器执行它来消除此错误。

var toRemoveList =
this.outputDic.Keys.Where(key =>
    this.removeDic.ContainsKey(key)).ToList();

请注意上述语句中的 ToList()。它将确保您的查询已被执行,并且您的列表在 toRemoveList 中。

于 2013-08-02T17:29:50.333 回答
2

LINQ 查询使用延迟执行。它逐个流式传输项目,并根据请求重新传输它们。所以是的,每次你尝试删除一个键时,它都会改变结果,这就是它抛出异常的原因。当您调用ToArray()它时,它会强制执行查询,这就是它起作用的原因。

编辑:这在某种程度上是对您的评论的回应。查看msdn 上的迭代器块,这是 for each 执行时使用的机制。您的查询只是变成了一个表达式树,并且过滤器、项目、操作等在检索到元素时将被一一应用于元素,除非不可能。

于 2013-08-02T17:29:50.227 回答