2008-11-21 11 views
7

ich oft diese rekursiv verwenden 'Besucher' in F #Schreiben des F # rekursive Ordner Besucher in C# - seq vs IEnumerable

let rec visitor dir filter= 
    seq { yield! Directory.GetFiles(dir, filter) 
      for subdir in Directory.GetDirectories(dir) do yield! visitor subdir filter} 

Vor kurzem habe ich zu arbeiten begann einige F # Funktionalität in C# auf die Umsetzung, und ich bin versucht, dies als IEnumerable zu reproduzieren, aber ich habe Schwierigkeiten, weiter als diese bekommen:

static IEnumerable<string> Visitor(string root, string filter) 
{ 
    foreach (var file in Directory.GetFiles(root, filter)) 
     yield return file; 
    foreach (var subdir in Directory.GetDirectories(root)) 
     foreach (var file in Visitor(subdir, filter)) 
      yield return file; 
} 

Was ich nicht verstehe ist, warum ich ein Doppel foreach in C# Version für die Rekursion zu tun haben, aber nicht in F # ... Führt das seq {} implizit zu einem 'concat'?

Antwort

12

yield! macht einen ‚abzuflachen‘ Betrieb, so dass es integriert die Sequenz, die Sie in die äußere Sequenz bestanden, implizit eine foreach über jedes Element der Sequenz und yield auf jeder durchführen.

+1

Also könnte ich tun .SelectMany, um dies in C# zu reproduzieren? – Benjol

+1

C# 'yield return 'kann immer nur einen Gegenstand nach dem anderen zurückgeben. – Sunlight

3

Es gibt keine einfache Möglichkeit, dies zu tun. Man könnte dies umgehen, indem ein C# Typ definiert, der entweder ein Wert oder eine Folge von Werten speichern kann - die F # Notation wäre es:

type EnumerationResult<'a> = 
    | One of 'a 
    | Seq of seq<'a> 

(dies in irgendeiner Weise zu C# übersetzen Sie :-))

Nun könnte man so etwas schreiben:

static IEnumerable<EnumerationResult<string>> Visitor 
     (string root, string filter) { 
    foreach (var file in Directory.GetFiles(root, filter)) 
     yield return EnumerationResult.One(file); 
     foreach (var subdir in Directory.GetDirectories(root)) 
      yield return EnumerationResult.Seq(Visitor(subdir, filter)) 
    } 
} 

um es zu nutzen, sollten Sie eine Funktion, die EnumerationResult flacht schreiben, die mit der folgenden Signatur eine Erweiterungsmethode in C# könnten:

Nun ist dies ein Teil, wo es schwierig wird - wenn Sie dies auf eine geradlinige Weise implementiert, würde es immer noch "forach" enthalten, um über die verschachtelten "Seq" Ergebnisse zu iterieren. Ich glaube jedoch, dass Sie eine optimierte Version schreiben könnten, die keine quadratische Komplexität hat.

Ok .. Ich denke, das ist ein Thema für einen Blog-Post, anstatt etwas, das hier vollständig beschrieben werden könnte :-), aber hoffentlich zeigt es eine Idee, die Sie versuchen können, zu folgen!

[EDIT: Aber natürlich können Sie auch naive Implementierung von „Flatten“ verwenden, die „Select“ nur, um die Syntax von C# Iterator Code schöner verwenden würde]

2

Im speziellen Fall von alle Dateien unter einem bestimmten Verzeichnis abrufen, this overload of Directory.GetFiles am besten funktioniert:

static IEnumerable<string> Visitor(string root, string filter) { 
    return Directory.GetFiles(root, filter, SearchOption.AllDirectories); 
} 


im allgemeinen Fall einen Baums von zählbaren Objekten durchlaufen, wird eine verschachtelte foreach-Schleife oder gleichwertig erforderlich (siehe auch: All About Iterators).


Edit: Added ein Beispiel für eine Funktion keinen Baum in einer Aufzählung zu glätten:

static IEnumerable<T> Flatten<T>(T item, Func<T, IEnumerable<T>> next) { 
    yield return item; 
    foreach(T child in next(item)) 
    foreach(T flattenedChild in Flatten(child, next)) 
     yield return flattenedChild; 
} 

Diese können Sie alle verschachtelten Dateien auszuwählen, wie zuvor:

static IEnumerable<string> Visitor(string root, string filter) { 
    return Flatten(root, dir => Directory.GetDirectories(dir)) 
    .SelectMany(dir => Directory.GetFiles(dir, filter)); 
} 
+1

Tatsächlich hat diese spezielle Überlastung ein ernsthaftes praktisches Problem; wenn nämlich * eine * Datei oder ein Verzeichnis innerhalb des Suchbereichs aufgrund eines zu langen Pfads ungültig ist oder der Benutzer nicht über die entsprechenden Berechtigungen oder eine andere E/A-Ausnahme verfügt, wird die gesamte Operation abgebrochen und gibt keine Ergebnisse zurück. Im Gegensatz dazu gibt es bei Verwendung einer manuell rekursiven Suche keine derartigen Probleme. Sie können versuchen, die Auflistung jedes Verzeichnisses einzeln abzufangen. –

2

In C# verwende ich den folgenden Code für diese Art von Funktion:

public static IEnumerable<DirectoryInfo> TryGetDirectories(this DirectoryInfo dir) { 
    return F.Swallow(() => dir.GetDirectories(),() => new DirectoryInfo[] { }); 
} 
public static IEnumerable<DirectoryInfo> DescendantDirs(this DirectoryInfo dir) { 
    return Enumerable.Repeat(dir, 1).Concat(
     from kid in dir.TryGetDirectories() 
     where (kid.Attributes & FileAttributes.ReparsePoint) == 0 
     from desc in kid.DescendantDirs() 
     select desc); 
} 

Dies adressiert IO-Fehler (was leider unvermeidlich passiert) und vermeidet Endlosschleifen aufgrund von symbolischen Links (insbesondere werden Sie bei der Suche nach einigen Verzeichnissen in Windows 7 darauf stoßen).

+0

Sowohl vollständig als auch prägnant – Hans