2009-07-13 25 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实现,这些实现完全符合上述内容,因为它将搜索目录/文件并生成它们在适当的时候,而不是前面加载所有结果。