Как сделать C # 'grep' более функциональным, используя LINQ? - PullRequest
9 голосов
/ 14 июля 2009

У меня есть метод, который выполняет упрощенный '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))));

Но я все еще теряю информацию, которая мне нужна.

Может быть, я просто подхожу к этому не с того угла? С точки зрения производительности циклы должны работать примерно в том же порядке, что и в исходном примере.

Есть идеи, как это сделать в более компактном функциональном представлении?

Ответы [ 2 ]

9 голосов
/ 14 июля 2009

Как насчет:

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

Edit:

Концептуально запрос превращает каждое имя файла в набор строк, а затем перекрестно соединяет этот набор строк с набором строк поиска (то есть каждая строка связана с каждой строкой поиска). Этот набор фильтруется по соответствующим строкам, и выбирается соответствующая информация для каждой строки.

Несколько предложений from аналогичны вложенным операторам foreach. Каждая указывает на новую итерацию в объеме предыдущей. Несколько предложений from преобразуются в метод SelectMany, который выбирает последовательность из каждого элемента и объединяет результирующие последовательности в одну последовательность.

Весь синтаксис запросов C # переводится в методы расширения. Тем не менее, компилятор использует некоторые приемы. Одним из них является использование анонимных типов. Всякий раз, когда переменные диапазона 2+ находятся в одной и той же области видимости, они, вероятно, являются частью скрытого анонимного типа. Это позволяет произвольным объемам данных в области пропускания проходить через методы расширения, такие как Select и Where, которые имеют фиксированное число аргументов. См. этот пост для получения дополнительной информации.

Вот перевод метода расширения указанного выше запроса:

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
    });
3 голосов
/ 14 июля 2009

Я бы использовал вызовы API FindFile (FindFirstFileEx, FindNextFile и т. Д. И т. Д.), Чтобы найти в файле искомый термин. Это, вероятно, сделает это быстрее, чем вы читаете построчно.

Однако, если это не сработает, вам следует подумать о создании реализации IEnumerable<String>, которая будет считывать строки из файла и выводить их по мере их чтения (вместо того, чтобы читать их все в массив). Затем вы можете запросить каждую строку и получить следующую, только если это необходимо.

Это должно сэкономить вам много времени.

Обратите внимание, что в .NET 4.0 многие API IO, которые возвращают строки из файлов (или файлов поиска), будут возвращать реализации IEnumerable, которые делают именно то, что упомянуто выше, в том смысле, что будут искать каталоги / файлы и выдавать их, когда уместно вместо фронтальной загрузки всех результатов.

...