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"
},
...
]
我们可以使用类将此文件读入内存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);
}
}
GetOrders将 JSON 文件的内容转换为 s 数组Order。请注意,它不是一个纯函数,因为它返回的顺序取决于 的内容Orders.json(该内容会随时间变化)。
我们的目标是创建“报告”,其中报告按国家/地区收集订单 ID。报告类型如下:1
Dictionary<string /*country*/, List<int> /*order IDs*/>
具体来说,我们想编写一个函数,该函数接受年份列表作为输入,并返回报告列表——每个给定年份对应一份报告。
命令式版本
在 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;
}
这种方法确实可行,但容易出错,因为我们是逐个订单地构建报告,很容易出错。对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();
这个版本有了很大的改进,因为我们不再需要逐个订单地生成报告。相反,我们可以考虑数据流,这是一种更高层次的抽象。对于每一年,我们都会获取一个订单流,使用 `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}");
}
}
这种方法还可以简化实施过程,因为一开始就按年份对订单进行分组,这样就无需每年重复处理所有订单。有人想在评论区尝试一下吗?
-
C# 的一个主要缺陷是缺少 typedef,因此没有简单的方法可以为这种类型创建缩写。C# 虽然有
using别名,但它们远不能替代 typedef。唉 ↩