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

时间:2008-11-13 15:13:50

标签: c# .net

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

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
}

如何以更好的方式重写此内容?我猜一个递归的解决方案?

11 个答案:

答案 0 :(得分:5)

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

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);
    }
}

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

答案 1 :(得分:4)

您可以使用三种策略。

  1. 复制列表&lt;&gt;到第二个集合(列表或数组 - 也许使用ToArray())。循环遍历第二个集合,将URL添加到第一个集合。
  2. 创建第二个列表&lt;&gt;,并循环显示您的网址列表&lt;&gt;将新值添加到第二个列表。完成循环后将它们复制到原始列表中。
  3. 使用 for 循环而不是 foreach 循环。提前计算你的数量。列表应该保留正确的索引,因此您可以添加它们将列在列表末尾的内容。
  4. 我更喜欢#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)假设您想要处理循环中找到的其他网址。如果执行希望在找到其他网址时处理其他网址,只需在 for 循环中使用urls.Count,而不是使用本地计数变量configurator在对此答案的评论中提及。

答案 2 :(得分:2)

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

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);

答案 3 :(得分:1)

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

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;

答案 4 :(得分:1)

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

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);
}

答案 5 :(得分:0)

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

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

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

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

答案 6 :(得分:0)

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

答案 7 :(得分:0)

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

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 :(得分:0)

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

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 :(得分:0)

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

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

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

答案 10 :(得分:0)

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

  1. 使用所有网址创建一个队列。
  2. 出列时,我们几乎都在说我们已经处理了它,所以把它添加到结果中。
  3. 如果GetLinks()返回任何内容,请将其添加到队列中并处理它们。
  4. 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。也许你想做的只是一级扩展。如果是这样,我们可以完全摆脱队列。

    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;
    }
    

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