2009-07-13 53 views
9

我有一個方法,使用一個「搜索字符串」可執行文件執行簡單的'grep'跨文件。 (實際上,我正在做一個很天真「查找所有引用」)如何使用LINQ使C#「grep」更具功能性?

IEnumerable<string> searchStrings = GetSearchStrings(); 
IEnumerable<string> filesToLookIn = GetFiles(); 
MultiMap<string, string> references = new MultiMap<string, string>(); 

foreach(string fileName in filesToLookIn) 
{ 
    foreach(string line in File.ReadAllLines(fileName)) 
    { 
     foreach(string searchString in searchStrings) 
     { 
      if(line.Contains(searchString)) 
      { 
       references.AddIfNew(searchString, fileName); 
      } 
     } 
    } 
} 

注:MultiMap<TKey,TValue>是大致相同Dictionary<TKey,List<TValue>>,只是避免你通常會遇到的NullReferenceException異常。


我一直在試圖把這個變成一個更「功能」的風格,使用鏈式LINQ擴展方法,但沒有弄清楚。

一個死衚衕嘗試:

// I get lost on how to do a loop within a loop here... 
// plus, I lose track of the file name 
var lines = filesToLookIn.Select(f => File.ReadAllLines(f)).Where(// ??? 

而另一家(希望保存的文件名這個時候):

var filesWithLines = 
    filesToLookIn 
     .Select(f => new { FileName = f, Lines = File.ReadAllLines(f) }); 

var matchingSearchStrings = 
    searchStrings 
     .Where(ss => filesWithLines.Any(
         fwl => fwl.Lines.Any(l => l.Contains(ss)))); 

但我仍然似乎失去我需要的信息。

也許我只是從錯誤的角度來解決這個問題?從性能角度來看,循環應該與原始示例的順序大致相同。

任何想法如何在一個更緊湊的功能表示?

回答

9

如何:

var matches = 
    from fileName in filesToLookIn 
    from line in File.ReadAllLines(fileName) 
    from searchString in searchStrings 
    where line.Contains(searchString) 
    select new 
    { 
     FileName = fileName, 
     SearchString = searchString 
    }; 

    foreach(var match in matches) 
    { 
     references.AddIfNew(match.SearchString, match.FileName); 
    } 

編輯:

從概念上講,查詢變成每個文件名成一組線,然後跨加入該組線,以該組搜索字符串(意思是每行與每個搜索字符串配對)。該集被過濾爲匹配行,並選擇每行的相關信息。

多個from子句類似於嵌套的foreach語句。每個表示在前一個範圍內的新迭代。將多個from子句轉換爲SelectMany方法,該方法從每個元素中選擇一個序列,並將結果序列展平成一個序列。

所有C#的查詢語法都轉換爲擴展方法。但是,編譯器的確使用了一些技巧。一種是使用匿名類型。每當2+範圍變量處於相同範圍內時,它們可能都是幕後匿名類型的一部分。這允許任意數量的範圍數據流過擴展方法,如SelectWhere,它們具有固定數量的參數。有關更多詳細信息,請參閱this post

下面是上面查詢的擴展方法翻譯:

var matches = filesToLookIn 
    .SelectMany(
     fileName => File.ReadAllLines(fileName), 
     (fileName, line) => new { fileName, line }) 
    .SelectMany(
     anon1 => searchStrings, 
     (anon1, searchString) => new { anon1, searchString }) 
    .Where(anon2 => anon2.anon1.line.Contains(anon2.searchString)) 
    .Select(anon2 => new 
    { 
     FileName = anon2.anon1.fileName, 
     SearchString = anon2.searchString 
    }); 
+1

我不知道,你可以用多個「從」這樣的語句。這實際上是如何工作的?我的LINQ經驗純粹是通過lambdas和擴展方法。這甚至轉化爲鏈式擴展方法嗎? – 2009-07-14 02:45:02

3

我會使用FindFile(FindFirstFileEx,FindNextFile等等)API調用在文件中查找您搜索的術語。它可能會比你逐行閱讀的速度更快。

但是,如果這對您不適用,您應該考慮創建一個IEnumerable<String>實現,它將讀取文件中的行並在讀取它們時產生它們(而不是將它們全部讀入到數組中)。然後,您可以查詢每個字符串,只有在需要時才能獲取下一個字符串。

這應該會爲您節省很多時間。

請注意,在.NET 4.0中,許多從文件(或搜索文件)返回行的IO apis將返回IEnumerable實現,這些實現完全符合上述內容,因爲它將搜索目錄/文件並生成它們在適當的時候,而不是前面加載所有結果。