2009-07-13 23 views
9

Ho un metodo che esegue un semplicistico "grep" tra i file, utilizzando una enumerabile di "stringhe di ricerca". (In effetti, sto facendo un molto ingenuo "Trova tutti i Riferimenti")Come rendere un C# 'grep' più funzionale usando LINQ?

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

Nota: MultiMap<TKey,TValue> è più o meno lo stesso di Dictionary<TKey,List<TValue>>, solo evitando le NullReferenceExceptions che normalmente si incontrano.


mi hanno cercato di mettere questo in uno stile più "funzionale", usando concatenati metodi di estensione LINQ, ma non hanno capito.

Un tentativo vicolo cieco:

// 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(// ??? 

E un altro (si spera preservare il nome del file questa volta):

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

Ma ho ancora sembra di perdere le informazioni di cui ho bisogno.

Forse mi sto avvicinando a questo dall'angolo sbagliato? Dal punto di vista delle prestazioni, i loop dovrebbero eseguire all'incirca lo stesso ordine dell'esempio originale.

Qualche idea su come eseguire questa operazione in una rappresentazione funzionale più compatta?

risposta

9

ne dite:

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:

Concettualmente, la query si trasforma ogni nome di file in un insieme di linee, poi cross-join che insieme di linee per l'insieme delle stringhe di ricerca (significa che ogni linea è abbinata a ciascuna stringa di ricerca). Quel set viene filtrato per corrispondere alle linee e vengono selezionate le informazioni rilevanti per ogni riga.

Le clausole multiple from sono simili alle istruzioni nidificate foreach. Ognuno indica una nuova iterazione nell'ambito del precedente. Più clausole from si traducono nel metodo SelectMany, che seleziona una sequenza da ciascun elemento e appiattisce le sequenze risultanti in una sequenza.

Tutta la sintassi della query di C# si traduce in metodi di estensione.Tuttavia, il compilatore utilizza alcuni trucchi. Uno è l'uso di tipi anonimi. Ogni volta che più di 2 variabili di intervallo sono nello stesso ambito, probabilmente fanno parte di un tipo anonimo dietro le quinte. Ciò consente alle quantità arbitrarie di dati con ambito di fluire attraverso i metodi di estensione come Select e Where, che hanno numeri fissi di argomenti. Vedi this post per ulteriori dettagli.

Ecco la traduzione metodo di estensione della query precedente:

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

Non ero a conoscenza si potrebbe usare più "da" dichiarazioni come queste. Come funziona davvero? La mia esperienza con LINQ è avvenuta esclusivamente attraverso lambda e metodi di estensione. Questo si traduce persino in metodi di estensione concatenati? –

+0

Sì, più da clausole traducono in chiamate al metodo di estensione SelectMany. Guardalo in Reflector per vedere esattamente cosa sta succedendo. – dahlbyk

+0

@jmitchem: ho modificato la mia risposta per rispondere alle vostre domande. –

3

Vorrei utilizzare le chiamate API FindFile (FindFirstFileEx, FindNextFile, ecc. Ecc.) Per cercare nel file il termine che si sta cercando. Probabilmente lo farà più velocemente di quanto tu possa leggere linea per linea.

Tuttavia, se ciò non dovesse funzionare, dovresti prendere in considerazione la creazione di un'implementazione IEnumerable<String> che legge le righe dal file e le restituisce mentre vengono lette (anziché leggerle tutte in un array). Quindi, è possibile eseguire una query su ogni stringa e ottenere il successivo solo se necessario.

Questo dovrebbe risparmiare un sacco di tempo.

Si noti che in .NET 4.0, molte delle API IO che restituiscono le righe dai file (o dai file di ricerca) restituiranno implementazioni IEnumerable che fanno esattamente ciò che è menzionato sopra, in quanto cercherà directory/file e li renderà se del caso invece di caricare frontalmente tutti i risultati.