【问题标题】:C# - match ID to filename using functional principlesC# - 使用功能原理将 ID 与文件名匹配
【发布时间】:2017-05-11 00:12:56
【问题描述】:

我正在从头开始重写一个旧的 C# 项目,试图弄清楚如何使用函数式设计来改进它。到目前为止,我坚持了几个原则(GUI 除外):

  • 将每个类中的每个变量都设置为readonly,并且只为其赋值一次。
  • 使用不可变集合
  • 不要编写带有副作用的代码。

现在我正在尝试创建一个函数,给定一个文件夹,使用 yield return 枚举一个对象列表,给定文件夹中的每个文件一个。每个对象都包含一个唯一的 ID(从 firstAssignedID 开始)和一个文件名。

问题是,我根本不知道如何解决这个问题。我刚才所描述的是否是正确的思考方式?到目前为止,我的代码是半生不熟的、不完整的混乱。可以在这里使用 lambda 吗?这会有所帮助,还是有更好的方法?

FileObject 类只包含一个string fileName 和一个int id,并且 FileObject 构造函数简单而天真地创建一个给定这两个值的实例。

    public IEnumerable<FileObject> EnumerateImagesInPath(string folderPath, int firstAssignedID)
    {
        foreach (string path in Directory.EnumerateFiles(folderPath)
        {
            yield return new FileObject(Path.GetFileName(imagePath) , );
        }
    }

【问题讨论】:

  • “功能设计”到底是什么意思?我可以对不同的人有不同的意思。
  • " 功能原则" - 我认为您的意思是最佳实践
  • 我正在学习,我并不清楚函数式编程是什么,但有人向我保证这是一件值得研究的事情。到目前为止,我将其视为您对编写代码的方式施加的一组约束,它允许您清楚地定义范围并跟踪状态。功能小而集中。您可以确定您的函数不会进行比看起来更多的修改。一个变量将只包含最初分配给它的一个值,因此您放置语句的顺序很明显 - 如果您以错误的顺序放置它们,则会导致编译器错误。
  • 这里的问题是,如果我要使用某种迭代器来生成一个 ID,那将是一个强制使用的变量(将其设置为这个值,现在设置为这个),我认为它是可以使用 LINQ 和/或 lambda 以更简单的方式完成。

标签: c# functional-programming ienumerable


【解决方案1】:

做你想做的最实用的方法是:

IEnumerable<FileObject> EnumerateImagesInPath(string path, int firstAssignedID) =>
    Enumerable.Zip(
        Enumerable.Range(firstAssignedID, Int32.MaxValue),
        Directory.EnumerateFiles(path), 
        FileObject.New);

FileObject 类型定义如下:

public class FileObject
{
    public readonly int Id;
    public readonly string Filename;

    FileObject(int id, string fileName)
    {
        Id = id;
        Filename = fileName;
    }

    public static FileObject New(int id, string fileName) =>
        new FileObject(id, fileName);
}

它不使用yield,但这没关系,因为Enumerable.RangeEnumerable.Zip 使用,所以它是一个惰性函数,就像你原来的例子一样。

我使用Enumerable.Range 创建一个惰性整数列表,从firstAssignedIdInt32.MaxValue。这与目录中的可枚举文件一起压缩。 FileObject.New(id. path) 作为 zip 计算的一部分被调用。

没有像接受的答案(firstAssignedID++)那样的就地状态修改,整个函数可以表示为一个表达式。

实现目标的另一种方法是使用fold 模式。它是函数式编程中最常见的聚合状态方式。这是为IEnumerable定义它的方法

public static class EnumerableExt
{
    public static S Fold<S, T>(this IEnumerable<T> self, S state, Func<S, T, S> folder) =>
        self.Any()
            ? Fold(self.Skip(1), folder(state, self.First()), folder)
            : state;
}

您应该能够看到它是一个递归函数,它在列表的头部运行一个委托 (folder),然后在递归调用 Fold 时将其用作新状态。如果到达列表的末尾,则返回聚合状态。

您可能会注意到 EnumerableExt.Fold 的实现可能会在 C# 中炸毁堆栈(因为缺少尾调用优化)。因此,实现Fold 函数的更好方法是强制执行:

public static S Fold<S, T>(this IEnumerable<T> self, S state, Func<S, T, S> folder)
{
    foreach(var x in self)
    {
        state = folder(state, x);
    }
    return state;
}

Fold 有一个对偶,称为FoldBack(有时它们被称为“向左折叠”和“向右折叠”)。 FoldBack 本质上是从列表的尾部到头部的聚合,其中Fold 是从头部到尾部的聚合。

public static S FoldBack<S, T>(this IEnumerable<T> self, S state, Func<S, T, S> folder)
{
    foreach(var x in self.Reverse())  // Note the Reverse()
    {
        state = folder(state, x);
    }
    return state;
}

Fold 非常灵活,例如,您可以为 fold 的可枚举实现 Count,如下所示:

int Count<T>(this IEnumerable<T> self) =>
    self.Fold(0, (state, item) => state + 1);

Sum 像这样:

int Sum<int>(this IEnumerable<int> self) =>
    self.Fold(0, (state, item) => state + item);

或者大部分IEnumerable API!

public static bool Any<T>(this IEnumerable<T> self) =>
    self.Fold(false, (state, item) => true);

public static bool Exists<T>(this IEnumerable<T> self, Func<T, bool> predicate) =>
    self.Fold(false, (state, item) => state || predicate(item));

public static bool ForAll<T>(this IEnumerable<T> self, Func<T, bool> predicate) =>
    self.Fold(true, (state, item) => state && predicate(item));

public static IEnumerable<R> Select<T, R>(this IEnumerable<T> self, Func<T, R> map) =>
    self.FoldBack(Enumerable.Empty<R>(), (state, item) => map(item).Cons(state));

public static IEnumerable<T> Where<T>(this IEnumerable<T> self, Func<T, bool> predicate) =>
    self.FoldBack(Enumerable.Empty<T>(), (state, item) => 
        predicate(item) 
            ? item.Cons(state)
            : state);

它非常强大,并且允许为集合聚合状态(因此这允许我们在没有必要的就地状态修改的情况下进行firstAssignedId++)。

我们的FileObject 示例比CountSum 稍微复杂一点,因为我们需要维护两个状态:聚合ID 和生成的IEnumerable&lt;FileObject&gt;。所以我们的状态是Tuple&lt;int, IEnumerable&lt;FileObject&gt;&gt;

IEnumerable<FileObject> FoldImagesInPath(string folderPath, int firstAssignedID) =>
    Directory.EnumerateFiles(folderPath)
             .Fold(
                  Tuple.Create(firstAssignedID, Enumerable.Empty<FileObject>()),
                  (state, path) => Tuple.Create(state.Item1 + 1, FileObject.New(state.Item1, path).Cons(state.Item2)))
             .Item2;

您可以通过为Tuple&lt;int, IEnumerable&lt;FileObject&gt;&gt; 提供一些扩展和静态方法来使其更具声明性:

public static class FileObjectsState
{
    // Creates a tuple with state ID of zero (Item1) and an empty FileObject enumerable (Item2)
    public static readonly Tuple<int, IEnumerable<FileObject>> Zero = 
        Tuple.Create(0, Enumerable.Empty<FileObject>());

    // Returns a new tuple with the ID (Item1) set to the supplied argument
    public static Tuple<int, IEnumerable<FileObject>> SetId(this Tuple<int, IEnumerable<FileObject>> self, int id) =>
        Tuple.Create(id, self.Item2);

    // Returns the important part of the result, the enumerable of FileObjects
    public static IEnumerable<FileObject> Result(this Tuple<int, IEnumerable<FileObject>> self) =>
        self.Item2;

    // Adds a new path to the aggregate state and increases the ID by one.
    public static Tuple<int, IEnumerable<FileObject>> Add(this Tuple<int, IEnumerable<FileObject>> self, string path) =>
        Tuple.Create(self.Item1 + 1, FileObject.New(self.Item1, path).Cons(self.Item2));
}

扩展方法捕获对聚合状态的操作,并使生成的fold 计算非常清晰:

IEnumerable<FileObject> FoldImagesInPath(string folderPath, int firstAssignedID) =>
    Directory.EnumerateFiles(folderPath)
             .Fold(
                FileObjectsState.Zero.SetId(firstAssignedID),
                FileObjectsState.Add)
             .Result();

显然,在您提供的用例中使用Fold 是多余的,这就是我改用Zip 的原因。但是您遇到的更普遍的问题(功能聚合状态)是 Fold 的用途。

我在上面的示例中使用了另外一种扩展方法:Cons:

public static IEnumerable<T> Cons<T>(this T x, IEnumerable<T> xs)
{
    yield return x;
    foreach(var a in xs)
    {
        yield return a;
    }
}

更多信息cons can be found here

如果您想了解有关在 C# 中使用函数式技术的更多信息,please check my library: language-ext。它会为您提供大量 C# BCL 所缺少的东西。

【讨论】:

  • 感谢您的回复。我现在无法深入回应,因为我在工作,但昨晚我尝试了你的第一个(更简单)解决方案,效果很好。我认为这种方式比 Daniel 的方式更实用 - 一个很好的原因是消除了迭代器变量,而且您的答案还引入了似乎非常适合这项工作的函数 (Range, Zip)。我不知道你可以使用 lambda 运算符创建一个非匿名函数。
  • 也许使用“更多功能”这个词是没有意义的。我的意思是解决方案更符合我的理想。
  • 我知道你的意思 :) 功能对我来说意味着: 1. 使用一流的函数 2. 在可能的情况下,一切都是纯表达式 3. 函数/方法是引用透明的(没有侧面-效果) 4. 数据结构是不可变的 除了 1,其他在大多数函数式语言中都是可选的。但我喜欢考虑这些最佳实践。你正在做正确的事情,试图让你的头脑进入函数式编程,它会给你作为编码员的超能力。我个人希望我作为程序员的前 20 年不是在命令式世界中。迟到总比没有好!
【解决方案2】:

似乎没有必要使用 yeild:

    public IEnumerable<FileObject> EnumerateImagesInPath(string folderPath, int firstAssignedID)
    {
        foreach (FileObject File in Directory.EnumerateFiles(folderPath)
            .Select(FileName => new FileObject(FileName, firstAssignedID++)))
        {
            yield return File;
        }
    }

【讨论】:

  • 这行得通!我要仔细调查一下。不过,如果您不介意,在我将其标记为已回答之前有几个问题:您是什么意思,使用 yield 似乎没有必要?还有另一种方法吗?此外,您对 ++ 运算符的使用是否被视为副作用或在任何方面都不好,因为它会反复更改 firstAssignedID 的值?
  • 据我所知,这会枚举目录中的所有文件并将它们存储在一个列表中,然后再产生第一个,这不是本意——一个一个会更理想。但我会看看我是否可以调整你的答案。
  • 我根据您的需要更改了答案,仍然使用 yield 似乎没有必要,因为 EnumerateFiles 不会进行任何类型的惰性评估:D
  • 不是吗? :( 你原来的答案同样有效。我们可以这样保留它吗,因为它会很好地推广到其他枚举器——除非它每次我们想要一个文件名时都在做 EnumerateFiles。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-05-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-03-31
  • 2019-08-09
  • 1970-01-01
相关资源
最近更新 更多