2008-11-13 60 views
2

下面的示例拋出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 
} 

我該如何以更好的方式重寫?我在猜測遞歸解決方案?

+0

你想蜘蛛整個互聯網或只是得到原來的列表中的網頁上的鏈接? – 2008-11-13 15:27:34

+0

呵呵,這只是一個例子,當然:) – 2008-11-13 15:28:55

+0

蜘蛛互聯網是有趣的;我開始一個過程,以蜘蛛www.altavista.com一次有趣,當我的硬盤滿了,我發現它主要是pr0n :) – configurator 2008-11-13 15:50:14

回答

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

這是稍微難看由於有不是在Queue<T>AddRange方法,但我認爲它基本上是你想要的。

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

有一個Queue類,但它沒有AddRange,所以這是更緊湊的代碼方式,但它們在功能上是等效的 – 2008-11-13 15:57:01

+0

需要5行代碼才能擴展Queue類以添加一個範圍,並且您應該可以在正在使用的類文件中執行此操作。 – 2008-11-13 17:00:54

+0

@ [Bill K]:但它不需要額外的代碼行來使用列表作爲隊列,但它仍然可以正常工作;-) – 2008-11-13 19:01:14

0

不要改變你正在通過循環通過每個集合。只需在列表的Count屬性上使用while循環並按索引訪問List項目。這樣,即使添加了項目,迭代也應該能夠獲取更改。

編輯:然後再次,這取決於你是否想要你添加的新項目被循環拾取。如果沒有,那麼這將無濟於事。

編輯2:我想這樣做是隻改變你的循環,以最簡單的方法: 的foreach(字符串URL中urls.ToArray())

這將創建一個列表的數組副本,它會循環而不是原來的列表。這會產生不會循環添加項目的效果。

0

考慮使用帶有while循環的隊列(而q.Count> 0,url = q.Dequeue())而不是迭代。

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

有三種策略可以使用。

  1. 將列表<>複製到第二個集合(列表或數組 - 可能使用ToArray())。循環播放第二個收藏集,爲第一個網址添加網址。
  2. 創建第二個列表<>,並遍歷您的URL列表<>向第二個列表中添加新值。完成循環後,將這些複製到原始列表。
  3. 使用代替代替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,因爲它們是在循環中發現。如果您希望,因爲他們被發現處理其他網址,只需使用urls.Count在循環而不是本地計數變量由configurator在此答案的評論中提到。

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做所有的工作。

但我可能錯過了你的計劃點。

0

喬恩的方法是對的;一個隊列是這種應用程序的正確數據結構。

假設你最終會喜歡你的程序終止,我建議其他兩件事情:

  • 不使用string您的網址,使用System.Web.Uri:它提供了一個規範的字符串表示URL。這對於第二個建議很有用,它是...
  • 將您在「詞典」中處理的每個URL的規範化字符串表示形式。排入URL之前,請先檢查它是否位於字典中。
0

如果不知道GetLinks()的作用,很難讓代碼更好。無論如何,這避免了遞歸。標準習慣用法是,當你列舉它時不要改變集合。雖然運行時可以讓你這樣做,但推理是它是錯誤的來源,所以最好創建一個新的集合或者自己控制迭代。

  1. 創建一個包含所有網址的隊列。
  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的。也許你想要做的只是一級擴展。如果是這樣,我們可以完全擺脫隊列。

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

我決定重寫它,因爲雖然其他答案使用隊列,但並不明顯他們沒有永遠運行。