在C#中编写F#递归文件夹访问者 – seq vs IEnumerable

我经常在F#中使用这个递归的“访客”

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

最近我开始在C#中实现一些F#function,我正在尝试将其重现为IEnumerable,但是我遇到的困难比这更进一步:

 static IEnumerable 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; } 

我不明白为什么我必须在C#版本中为递归做一个双重foreach,而不是在F#中… seq {}是否隐含地执行’concat’?

yield! 执行’flatten’操作,因此它将您传递给外部序列的序列集成在一起,隐式地对序列的每个元素执行foreach在每个元素上yield

没有简单的方法可以做到这一点。 您可以通过定义可以存储一个值或一系列值的C#类型来解决此问题 – 使用F#表示法:

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

(以你喜欢的任何方式将其转换为C#:-))

现在,你可以这样写:

 static IEnumerable> 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)) } } 

要使用它,你必须编写一个扁平化EnumerationResult的函数,它可以是C#中的扩展方法,具有以下签名:

 IEnumerable Flatten(this IEnumerable> res); 

现在,这是一个棘手的部分 – 如果你以一种直接的方式实现它,它仍然包含“forach”迭代嵌套的“Seq”结果。 但是,我相信您可以编写一个不具有二次复杂度的优化版本。

好吧..我想这是一个博客文章的主题,而不是可以在这里完整描述的东西:-),但希望,它显示了一个想法,你可以尝试跟随!

[编辑:当然,你也可以使用“Flatten”的天真实现,使用“SelectMany”只是为了使你的C#迭代器代码的语法更好]

在检索特定目录下的所有文件的特定情况下, Directory.GetFiles这种重载效果最好:

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

在遍历可枚举对象树的一般情况下,需要嵌套的foreach循环或等效项(另请参阅: All About Iterators )。


编辑:添加了一个函数示例,将任何树展平为枚举:

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

这可用于选择所有嵌套文件,如前所述:

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

在C#中,我使用以下代码来实现这种function:

 public static IEnumerable TryGetDirectories(this DirectoryInfo dir) { return F.Swallow(() => dir.GetDirectories(), () => new DirectoryInfo[] { }); } public static IEnumerable 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); } 

这解决了IO错误(不幸的是,它不可避免地发生),并且避免了由于符号链接而导致的无限循环(特别是,您将遇到在Windows 7中搜索某些目录)。