【问题标题】:How to add items to a collection while consuming it?如何在消费时将项目添加到集合中?
【发布时间】:2010-09-22 04:18:22
【问题描述】:

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

var urls = new List<string>();
urls.Add("http://www.google.com");

foreach (string url in urls)
{
    // Get all links from the url
    List<string> newUrls = GetLinks(url);

    urls.AddRange(newUrls); // <-- This is really the problematic row, adding values to the collection I'm looping
}

如何以更好的方式重写它?我猜是递归解决方案?

【问题讨论】:

  • 你是想爬取整个互联网还是只获取原始列表中页面上的链接?
  • 呵呵,这只是一个例子,当然:)
  • 爬网很有趣;为了好玩,我开始了一次爬取 www.altavista.com 的过程,当我的硬盘驱动器已满时,我发现它主要是 pr0n :)

标签: c# .net


【解决方案1】:

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

var urls = new Queue<string>();
urls.Enqueue("http://www.google.com");

while(urls.Count != 0)
{
    String url = url.Dequeue();
    // Get all links from the url
    List<string> newUrls = GetLinks(url);
    foreach (string newUrl in newUrls)
    {
        queue.Enqueue(newUrl);
    }
}

由于Queue&lt;T&gt; 中没有AddRange 方法,因此有点难看,但我认为这基本上是您想要的。

【讨论】:

  • 可以总是正确的扩展方法 public void AddRange(this Queue queue, IEnumerable items) { foreach (T item in items) { queue.Enqueue(item); } }
  • FWIW 此代码的意图略有不同。 @Kaboing 正在附加 GetLinks() 的结果,但在处理后不丢弃“url”。
  • 非常正确。我希望我的代码是实际需要的代码,因为这样可以减少内存占用。如果提问者确实想要完整的 URL 列表,那么其他答案之一会更合适。
  • 我不知道 Queue 类。这正是我想要的。谢谢。
【解决方案2】:

您可以使用三种策略。

  1. 将 List 复制到第二个集合(列表或数组 - 可能使用 ToArray())。循环遍历第二个集合,将 url 添加到第一个集合。
  2. 创建第二个列表,然后遍历您的 url 列表,将新值添加到第二个列表。完成循环后将它们复制到原始列表。
  3. 使用 for 循环而不是 foreach 循环。提前数数。列表应该正确地对事物进行索引,因此您添加的事物将位于列表的末尾。

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

var urls = new List<string>();
urls.Add("http://www.google.com");
int count = urls.Count;

for (int index = 0; index < count; index++)
{
    // Get all links from the url
    List<string> newUrls = GetLinks(urls[index]);

    urls.AddRange(newUrls);
}

编辑:最后一个示例 (#3) 假设您不想处理在循环中发现的其他 URL。如果您确实想要在找到其他 URL 时对其进行处理,只需在 for 循环中使用 urls.Count 而不是本地 count 变量作为configurator 在 cmets 中提到了这个答案。

【讨论】:

  • 那么不要预先抓取计数 - 将 index
【解决方案3】:

将 foreach 与 lambda 一起使用,更有趣!

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

【讨论】:

  • 呃,这不会按预期工作 - 您将访问第一个 url 列表,并收集第二个 url 列表,然后停止!
  • 真正的男人,忘记在 foreach 之后将新列表添加到原始列表中。我会解决的。
【解决方案4】:

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

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

【讨论】:

  • 有一个 Queue 类,但它没有 AddRange,所以这在代码方面更紧凑,但它们在功能上是等效的
  • 扩展 Queue 类以添加范围大约需要 5 行代码,并且您应该能够在您正在使用的类文件中执行此操作。
  • @[Bill K]:但将列表用作队列不需要额外的代码行,但它仍然可以正常工作;-)
【解决方案5】:

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

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

【讨论】:

    【解决方案6】:

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

    【讨论】:

      【解决方案7】:

      我假设您想遍历整个列表,以及添加到其中的每个项目?如果是这样,我会建议递归:

      var urls = new List<string>();
      var turls = new List<string();
      turls.Add("http://www.google.com")
      
      iterate(turls);
      
      function iterate(List<string> u)
      {
          foreach(string url in u)
          {
              List<string> newUrls = GetLinks(url);
      
              urls.AddRange(newUrls);
      
              iterate(newUrls);
          }
      }
      

      【讨论】:

        【解决方案8】:

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

        IEnumerable<string> 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<string> MyEnumerateFunction()
        {
          return new List<string>(GetUrl("http://www.google.com"));
        }
        

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

        但我可能错过了你程序的重点。

        【讨论】:

          【解决方案9】:

          不要更改您通过 for each 循环遍历的集合。只需在列表的 Count 属性上使用 while 循环并按索引访问列表项。这样,即使您添加了项目,迭代也应该接受更改。

          编辑:再说一次,这有点取决于您是否希望循环拾取添加的新项目。如果没有,那么这将无济于事。

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

          这将为您的列表创建一个 Array 副本,并将循环遍历此列表而不是原始列表。这将不会循环您添加的项目。

          【讨论】:

          • 这是消耗物品列表的危险方式。
          【解决方案10】:

          Jon 的做法是正确的;队列是此类应用程序的正确数据结构。

          假设您最终希望程序终止,我建议您做两件事:

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

          【讨论】:

            【解决方案11】:

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

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

            .

            public List<string> ExpandLinksOrSomething(List<string> urls)
            {
                List<string> result = new List<string>();
                Queue<string> queue = new Queue<string>(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<string> newItems = GetLinks(url).Except(result).ToList();
                    foreach( string newResult in newItems )
                    {
                        queue.Enqueue(newResult);
                    }
            

            * 正如其他人指出的那样,根据您处理的项目数量,使用字典可能更有效。


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

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

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2010-11-15
              • 1970-01-01
              • 1970-01-01
              • 2015-12-10
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多