发布于 2026-01-06 0 阅读
0

C# 函数式编程 使用 JSON 编写报告 命令式版本 函数式版本 DEV 全球展示挑战赛 由 Mux 呈现:展示你的项目!

C# 中的函数式编程

使用 JSON 编写报告

命令式版本

功能版本

由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!

函数式编程依赖于纯函数,纯函数没有副作用,并且对于给定的输入总是返回相同的输出。这种编程范式有很多优点,但在 C# 中实现起来可能比较困难,尤其对于习惯于编写命令式代码的人来说更是如此。例如,如果你发现自己编写或使用了返回 `null` 的方法void,那么你的代码很可能不是函数式的。这种情况在构建复杂的数据结构时经常发生。

使用 JSON 编写报告

假设我们已将客户订单存储在一个名为 `<customer_orders_name>` 的 JSON 文件中Orders.json,如下所示:

[
  {
    "OrderID": 10248,
    "OrderDate": "1996-07-04T00:00:00",
    "ShipCountry": "France"
  },
  {
    "OrderID": 10249,
    "OrderDate": "1996-07-05T00:00:00",
    "ShipCountry": "Germany"
  },
  ...
]
Enter fullscreen mode Exit fullscreen mode

我们可以使用类将此文件读入内存Order

class Order
{
    public int OrderID { get; set; }
    public DateTime OrderDate { get; set; }
    public string ShipCountry { get; set; }

    public static Order[] GetOrders()
    {
        var json = File.ReadAllText("Orders.json");
        return JsonSerializer.Deserialize<Order[]>(json);
    }
}
Enter fullscreen mode Exit fullscreen mode

GetOrders将 JSON 文件的内容转换为 s 数组Order。请注意,它不是一个纯函数,因为它返回的顺序取决于 的内容Orders.json(该内容会随时间变化)。

我们的目标是创建“报告”,其中报告按国家/地区收集订单 ID。报告类型如下:1

Dictionary<string /*country*/, List<int> /*order IDs*/>
Enter fullscreen mode Exit fullscreen mode

具体来说,我们想编写一个函数,该函数接受年份列表作为输入,并返回报告列表——每个给定年份对应一份报告。

命令式版本

在 C# 中实现此类功能的传统方法是使用嵌套循环:

static List<Dictionary<string, List<int>>> GetReports(IList<int> years)
{
    var dicts = new List<Dictionary<string, List<int>>>();
    foreach (var year in years)
    {
        var dict = new Dictionary<string, List<int>>();
        foreach (var order in Order.GetOrders())
        {
            if (order.OrderDate.Year == year)
            {
                if (!dict.TryGetValue(order.ShipCountry, out List<int> orderIDs))
                {
                    dict[order.ShipCountry] = orderIDs = new List<int>();
                }
                orderIDs.Add(order.OrderID);
            }
        }
        dicts.Add(dict);
    }
    return dicts;
}
Enter fullscreen mode Exit fullscreen mode

这种方法确实可行,但容易出错,因为我们是逐个订单地构建报告,很容易出错。对List.Add`and`的调用Dictionary.Item(即使用方括号设置键值)会返回void`null`——它们不是纯函数。TryGetValue此外,`and` 在 C# 中也出了名的棘手,因为它糟糕的函数签名可能会在参数中留下一个必须谨慎处理的null值。out

请注意,这个版本的GetReports本身也不是纯粹的。因为它Order.GetOrders()直接调用,所以它的行为也取决于的内容Orders.json

功能版本

幸运的是,我们可以重写这段代码,使其仅使用纯函数,并且它本身也是一个纯函数。LINQ 是函数式编程 API 的一个绝佳示例,因此让我们用它来创建报表:

static List<Dictionary<string, List<int>>> GetReports(IList<Order> orders, IList<int> years)
    => years
        .Select(year =>
            orders
                .Where(order => order.OrderDate.Year == year)
                .GroupBy(
                    order => order.ShipCountry,
                    order => order.OrderID)
                .ToDictionary(
                    group => group.Key,
                    group => group.ToList()))
        .ToList();
Enter fullscreen mode Exit fullscreen mode

这个版本有了很大的改进,因为我们不再需要逐个订单地生成报告。相反,我们可以考虑数据流,这是一种更高层次的抽象。对于每一年,我们都会获取一个订单流,使用 `filter` 函数过滤掉不需要的订单Where,使用 `group` 函数按国家/地区分组GroupBy,然后使用 `todict` 函数将结果流转换为字典ToDictionary。由于不再包含嵌套循环或if语句,控制流变得非常简单。

请注意,我们还orders为该函数添加了一个显式参数,因此现在可以保证对于给定的订单和年份组合,它始终返回相同的报告。这使其成为一个纯函数,我们可以使用如下代码进行测试:

var years = new int[] { 1996, 1997, 1998 };
foreach (var dict in GetReports(Order.GetOrders(), years))
{
    Console.WriteLine();
    foreach (var pair in dict)
    {
        Console.WriteLine($"{pair.Key}: {pair.Value.Count}");
    }
}
Enter fullscreen mode Exit fullscreen mode

这种方法还可以简化实施过程,因为一开始就按年份对订单进行分组,这样就无需每年重复处理所有订单。有人想在评论区尝试一下吗?


  1. C# 的一个主要缺陷是缺少 typedef,因此没有简单的方法可以为这种类型创建缩写。C# 虽然有using别名,但它们远不能替代 typedef。 

文章来源:https://dev.to/shimmer/function-programming-in-c-3h6e