如何在消费时将项目添加到集合中?

下面的示例抛出InvalidOperationException,“Collection已被修改;枚举操作可能无法执行”。 执行代码时。

var urls = new List(); urls.Add("http://www.google.com"); foreach (string url in urls) { // Get all links from the url List newUrls = GetLinks(url); urls.AddRange(newUrls); // <-- This is really the problematic row, adding values to the collection I'm looping } 

我怎样才能以更好的方式重写这个? 我猜一个递归的解决方案?

基本上你不能。 你真正想要的是一个队列:

 var urls = new Queue(); urls.Enqueue("http://www.google.com"); while(urls.Count != 0) { String url = url.Dequeue(); // Get all links from the url List newUrls = GetLinks(url); foreach (string newUrl in newUrls) { queue.Enqueue(newUrl); } } 

由于Queue没有AddRange方法,它有点难看,但我认为它基本上就是你想要的。

您可以使用三种策略。

  1. 将List <>复制到第二个集合(列表或数组 – 也许使用ToArray())。 循环遍历第二个集合,将URL添加到第一个集合。
  2. 创建第二个List <>,并遍历您的URL列表<>将新值添加到第二个列表。 完成循环后将它们复制到原始列表。
  3. 使用for循环而不是foreach循环。 提前计算你的数量。 列表应该保留正确的索引,因此您可以添加它们将在列表末尾添加的内容。

我更喜欢#3,因为它没有任何与#1或#2相关的开销。 这是一个例子:

 var urls = new List(); urls.Add("http://www.google.com"); int count = urls.Count; for (int index = 0; index < count; index++) { // Get all links from the url List newUrls = GetLinks(urls[index]); urls.AddRange(newUrls); } 

编辑:最后一个示例(#3)假定您希望处理循环中找到的其他URL。 如果您确实希望在找到它们时处理其他URL,只需在for循环中使用urls.Count,而不是配置程序在此答案的注释中提到的本地计数变量。

使用带有lambda的foreach,它更有趣!

 var urls = new List(); var destUrls = new List(); urls.Add("http://www.google.com"); urls.ForEach(i => destUrls.Add(GetLinks(i))); urls.AddRange(destUrls); 

或者,您可以将集合视为队列

 IList urls = new List(); urls.Add("http://www.google.com"); while (urls.Count > 0) { string url = urls[0]; urls.RemoveAt(0); // Get all links from the url List newUrls = GetLinks(url); urls.AddRange(newUrls); } 

我会创建两个列表添加到第二个,然后像这样更新引用:

 var urls = new List(); var destUrls = new List(urls); urls.Add("http://www.google.com"); foreach (string url in urls) { // Get all links from the url List newUrls = GetLinks(url); destUrls.AddRange(newUrls); } urls = destUrls; 

考虑使用带有while循环的Queue(而q.Count> 0,url = q.Dequeue())而不是迭代。

我假设你想迭代整个列表,你添加的每个项目? 如果是这样我会建议递归:

 var urls = new List(); var turls = new List u) { foreach(string url in u) { List newUrls = GetLinks(url); urls.AddRange(newUrls); iterate(newUrls); } } 

你也可以创建一个递归函数,像这样(未经测试):

 IEnumerable GetUrl(string url) { foreach(string u in GetUrl(url)) yield return u; foreach(string ret_url in WHERE_I_GET_MY_URLS) yield return ret_url; } List MyEnumerateFunction() { return new List(GetUrl("http://www.google.com")); } 

在这种情况下,您不必创建两个列表,因为GetUrl完成所有工作。

但我可能已经错过了你的计划。

不要更改每个循环的集合。 只需在列表的Count属性上使用while循环,然后按索引访问List项。 这样,即使您添加项目,迭代也应该获取更改。

编辑:然后再次,它取决于你是否想要通过循环拾取你添加的新项目。 如果没有,那么这将无济于事。

编辑2:我想最简单的方法就是将你的循环改为:foreach(url.ToArray()中的字符串url)

这将创建列表的Array副本,它将遍历此列表而不是原始列表。 这将导致不循环添加的项目。

乔恩的做法是正确的; 队列是这种应用程序的正确数据结构。

假设你最终希望你的程序终止,我建议另外两件事:

  • 不要使用string作为您的URL,使用System.Web.Uri :它提供URL的规范字符串表示。 这对于第二个建议很有用,它是……
  • 将您处理的每个URL的规范字符串表示放在Dictionary中。 在将URL排入队列之前,请先检查它是否在“词典”中。

如果不知道GetLinks()的作用,很难使代码更好。 无论如何,这可以避免递归。 标准习惯用法是当你对它进行枚举时不要改变它。 虽然运行时可以让你这样做,但推理是它是错误的来源,所以最好自己创建一个新的集合或控制迭代。

  1. 使用所有url创建一个队列。
  2. 在出列时,我们几乎都在说我们已经处理了它,所以将它添加到结果中。
  3. 如果GetLinks()返回任何内容,请将它们添加到队列中并处理它们。

 public List ExpandLinksOrSomething(List urls) { List result = new List(); Queue queue = new Queue(urls); while (queue.Any()) { string url = queue.Dequeue(); result.Add(url); foreach( string newResult in GetLinks(url) ) { queue.Enqueue(newResult); } } return result; } 

天真的实现假设GetLinks()不会返回循环引用。 例如A返回B,B返回A.这可以通过以下方式修复:

  List newItems = GetLinks(url).Except(result).ToList(); foreach( string newResult in newItems ) { queue.Enqueue(newResult); } 

*正如其他人指出使用字典可能会更有效,具体取决于您处理的项目数量。


我觉得很奇怪,GetLinks()会返回一个值,然后再将其解析为更多的Url。 也许你想做的只是一级扩展。 如果是这样,我们可以完全摆脱队列。

 public static List StraightProcess(List urls) { List result = new List(); foreach (string url in urls) { result.Add(url); result.AddRange(GetLinks(url)); } return result; } 

我决定重写它,因为虽然其他答案使用了队列,但不明显它们没有永远运行。