5.高阶函数

高阶函数这个名字对于一个非常简单的东西来说有点奇怪。事实上,如果你花了很多时间使用 LINQ,那么你可能已经使用它们一段时间了。它们有两种形式,下面是第一种:

var liberatorCrew = new []
{
   
 "Roj Blake",
 "Kerr Avon",
 "Vila Restal",
 "Jenna Stannis",
 "Cally",
 "Olag Gan",
 "Zen"
};
var filteredList = liberatorCrew.Where(x => x.First() > 'm');

传递到 Where 函数的是一个箭头表达式 - 这只是写出函数的简写。完整版本如下所示:

function bool IsGreaterThanM(char c)
{
   
    return c > 'm';
}

因此,这里,该函数已作为参数传递给另一个函数,并在该函数内部的其他地方执行。

高阶函数的另一种形式如下:

public Func<int, int> MakeAddFunc(int x) => y => x + y;

在这里,我们使用代码创建一个 Func 委托并将其作为另一个函数的返回类型返回,这次将在创建它的函数的某个外部使用。

简而言之,高阶函数是作为变量传递的函数。这可以是另一个函数的返回类型或参数。它们在 C# 中以多种方式之一实现,但在现代 .NET 代码中最常见的是 FuncAction 委托类型。

这是一个相当简单的想法,甚至更容易实现 - 但它们对您的代码库的影响是惊人的。

在本章中,我将介绍使用高阶函数来改进您的日常编码的方法。

我还将深入研究高阶函数的下一级用法,称为组合器。它们允许以创建更复杂且更有用的行为的方式传递函数。顺便说一下,它们之所以这样称呼,是因为它们源自一种称为组合逻辑的数学技术。您无需担心会再次听到该术语,也无需担心任何与高等数学相关的内容 - 我不会去那里。这只是以防万一您好奇……

问题报告

首先,我们将查看一些问题代码。假设您的公司要求您提供一项功能,用于获取数据库表,汇总每个可能值的数量,然后将该数据传输到其他地方。最重要的是,他们希望在未找到任何数据的情况下发送一条单独的消息。我经营着一艘非常非常松散的飞船,所以让我们让事情变得有趣起来,想象一下你为邪恶银河帝国™工作,你正在你的雷达[1](file:///C:/Users/OneKing/AppData/Local/Temp/2hdnhmDeTz4ZeaPrsAgRP7gsBZi/resources/app.asar/build/ch05.xhtml#idm45858956404768)上对反叛联盟飞船进行分类。

代码可能看起来像这样:

public void GenerateEnemyShipTypeSumary()
{
   
    try
    {
   
        var enemyShips = this.DataStore.GetEnemyShips();
        var summaryNumbers = enemyShips.GroupBy(x => x.Type)
            .Select(x => (Type: x.Key, Count: x.Count()));
        var report = new Report
        {
   
            Title = "Enemy Ship Type",
            Rows = summaryNumbers.Select(X => new ReportItem
                                         {
   
                                             ColumnOne = X.Type,
                                             ColumnTwo = X.Count.ToString()
                                         })
        };

        if (report.Rows.Any())
            this.CommunicationSystem.SendNoDataWarning();
        else
            this.CommunicationSystem.SendReport(report);
    }
    catch (Exception e)
    {
   
        this.Logger.LogError(e,
                             $"An error occurred in {
     nameof(GenerateEnemyShipTypeSumary)}: {
     e.Message}");
    }
}

这很好,不是吗?不是吗?好吧,想想这个场景。你正坐在办公桌前,吃着每天吃的泡面,这时你注意到——侏罗纪公园风格——咖啡里出现了有节奏的涟漪。这预示着你最可怕的噩梦的到来。你的老板!让我们想象一下,你的老板——完全随机地想——是一位身材高大、声音低沉、身披黑色斗篷、哮喘严重的绅士。他也非常讨厌别人不高兴。真的讨厌。

他对你创建的第一个函数很满意。为此,你可以松一口气了。但现在他想要第二个函数。这个函数将创建另一个摘要,但这次是每艘飞船的武器等级。它们是没有武器、轻武器、重武器还是能够摧毁行星。诸如此类。

你认为这很容易。老板会对我做这件事的速度印象深刻。因此,您可以执行看似最简单的操作“Ctrl+C”,然后按“Ctrl+V”复制并粘贴原始内容,更改名称,更改您要总结的属性,最后得到如下结果:

public void GenerateEnemyShipWeaponrySumary()
{
   
    try
    {
   
        var enemyShips = this.DataStore.GetEnemyShips();
        var summaryNumbers = enemyShips.GroupBy(x => x.WeaponryLevel)
            .Select(x => (Type: x.Key, Count: x.Count()));
        var report = new Report
        {
   
            Title = "Enemy Ship Weaponry Level",
            Rows = summaryNumbers.Select(X => new ReportItem
                                         {
   
                                             ColumnOne = X.Type,
                                             ColumnTwo = X.Count.ToString()
                                         })
        };

        if (report.Rows.Any())
            this.CommunicationSystem.SendNoDataWarning();
        else
            this.CommunicationSystem.SendReport(report);
    }
    catch (Exception e)
    {
   
        this.Logger.LogError(e,
                             $"An error occurred in {
     nameof(GenerateEnemyShipWeaponrySumary)}: {
     e.Message}");
    }
}

五秒钟的工作,一两天的时间靠在你的铲子上,偶尔大声抱怨这里的工作有多辛苦,而你却在偷偷地开发今天的 Wordle。工作完成了,大家都很满意,对吧?对吧?

嗯……这种方法有几个问题。

首先,让我们考虑一下单元测试。作为优秀、正直的代码公民,我们会对所有代码进行单元测试。想象一下,我们对第一个函数进行了彻底的单元测试。当我们复制并粘贴第二个函数时,那时的单元测试覆盖率是多少?

我给你一个提示——它在零和零之间。你也可以复制和粘贴测试,那也没问题,但现在我们每次都要复制和粘贴的代码要多得多。

这种方法不是一个很好的扩展方法。如果我们的老板想要在这个函数之后再添加一个函数,然后又添加一个,再添加一个,该怎么办。如果我们最终被要求提供 50 个函数怎么办?或者 100 个?!那代码太多了。你最终会得到数千行代码,而我并不想支持它。

当你想到我职业生涯初期发生的事情时,情况会变得更糟。我当时在一家组织工作,该组织有一个桌面应用程序,可以根据一些输入参数为每个客户执行一系列复杂的计算。每年规则都会改变,但必须复制旧的规则库,因为可能需要查看前一年计算的内容。

因此,在我加入团队之前开发应用程序的人每年都会复制一大段代码。做了一些小改动,在某处添加了指向新版本的链接,然后就大功告成了。

有一年,我被要求进行这些年度变革,所以我就出发了,年轻、天真,渴望为世界做出改变。当我进行改变时,我注意到了一些奇怪的事情。有一个字段出现了奇怪的错误,但与我的更改无关。我修复了这个错误,但随后我突然想到了一个让我心灰意冷的想法……

我检查了前一年代码库的每个版本,发现几乎所有版本都有相同的错误。它是在大约 10 年前引入的,从那时起,每个开发人员都精确地复制了这个错误。所以,我不得不修复它 10 次,将测试工作量增加了一个数量级。

考虑到这一点,问问自己 - 复制和粘贴真的为你节省了时间吗?我经常开发的应用程序已经存在了几十年,而且没有任何迹象表明它们很快就会被淘汰。

当我决定在哪里采取节省编码工作时间的措施时,我会尝试查看应用程序的整个生命周期,并尝试记住十年后做出的决定可能带来的后果。

回到手头的话题,我将如何使用高阶函数来解决这个问题?好吧,你坐得舒服吗?然后我开始……

Thunks

带有存储计算的代码包,可以在以后根据请求执行,这被称为 Thunk。就像木板击中头部时发出的声音一样。有人争论说这比读这本书更伤脑筋还是更伤脑筋!

在 C# 中,Func 委托再次是我们实现这一点的方式。我们可以编写以 Func 委托为参数的函数,以允许函数中的某些计算有效地留空,并且可以通过箭头函数从外部世界填充。

虽然这种技术有一个严肃、合适的数学术语,但我喜欢称它们为甜甜圈函数,因为它更具描述性。它们就像普通函数一样,但中间有一个洞!我会请别人用必要的功能填补这个洞。

这是重构问题报告函数的一种潜在方法:

public void GenerateEnemyShipTypeSumary() =>
    GenerateSumary(x => x.Type, "Enemy Ship Type Summary");

public void GenerateEnemyShipWeaponryLevelSummary() =>
    GenerateSumary(x => x.WeaponryLevel, "Enemy Ship WeaponryLevel");

private void GenerateSumary(Func<EnemyShip, string> summarySelector, string reportName)
{
   
    try
    {
   
        var enemyShips = this.DataStore.GetEnemyShips();
        var summaryNumbers = enemyShips.GroupBy(summarySelector)
            .Select(x => (Type: x.Key, Count: x.Count()));
        var report = new Report
        {
   
            Title = reportName,
            Rows = summaryNumbers.Select(X => new ReportItem
                                         {
   
                                             ColumnOne = X.Type,
                                             ColumnTwo = X.Count.ToString()
                                         })
        };

        if (report.Rows.Any())
            this.CommunicationSystem.SendNoDataWarning();
        else
            this.CommunicationSystem.SendReport(report);
    }
    catch (Exception e)
    {
   
        this.Logger.LogError(e,
                             $"An error occurred in {
     nameof(GenerateSumary)}, report: {
     reportName}, message: {
     e.Message}");
    }
}

在这个修订版本中,我们获得了一些优势。

首先,每个新报告的附加行数只有一行!这是一个更加整洁的代码库,也更容易阅读。代码与新功能的意图非常接近 - 即与第一个相同,但有一些变化。

其次,在对功能 1 进行单元测试后,当我们创建功能 2 时,单元测试级别仍然接近 100%。功能上唯一的区别是报告名称和要汇总的字段。

最后,对基本功能的任何增强或错误修复都将同时在所有报告功能之间共享。这花费了相对较少的努力,却获得了很大的好处。如果一个报告功能测试良好,那么所有其他功能也将相同,这一点也非常有信心。

人们实际上可以满意地放弃这个版本。但是,如果是我,我实际上会考虑更进一步,在界面上公开私有版本及其 Func 参数,以供任何想要使用它的人使用。

就像这样:

public interface 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

0neKing2017

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值