• 函数式编程实践(1):高阶函数的使用


    函数式编程与我们的距离并不遥远。虽然大部分人不会选择去学习F#,但是函数式编程的思想可以用C#来实践。自从C#3.0引入了Lambda表达式,虽然是作为LINQ的配角,但是其带来的改变远远超出LINQ的范围,C#从此一只脚迈入了FP的领地。

    回想Lambda在LINQ中的使用,我们就可轻易理解函数式编程的特征之一:函数作为参数传递(即“函数的函数”,函数作为自变量)。函数作为参数导致函数的复合(还记得高中数学中的f(g(x))吗)。经常用LINQ的人一定对此已经习以为常。

    今天我们讨论函数式编程的另外一个特征,即高阶函数。所谓高阶函数,就是“返回函数的函数”(注意与上文“函数的函数”相区别,函数作为因变量)。举一个最简单的例子:

    Func<int, Func<int, int>> f = x => y => x + y;
    

    注意这个委托变量的声明方式,从中我们可以直接解读到,这个委托类型对应的函数是这样的:输入一个int,返回一个函数;返回的函数是这样的:输入一个int,返回一个int。

    再看“f =”右边的Lambda表达式,它其实是这样:

    x => (y => (x + y));
    

    或者

    x => { return y => x + y; };
    

    这个函数就是一个返回函数的函数,即高阶函数。当我们输入外层函数的参数,例如指定1,则f(1)仍然是一个函数,它等于y => 1 + y这个函数,如果你乐意,可以给它取名叫g函数,它用于给一个数加1。 我们可以继续传入参数,例如f(1)(2),也就是g(2),也就是给2加1。

     

     

    不知道这样解释是否清楚明白。也许你会说这有什么用,你永远不会用到。但是我就是这样一个人,会对不一样的体验着迷,我把这种方式用到了项目中。我的项目是城市规划应用,是一个Windows Forms实现的城市指标分析结果查看器。在项目中有如下的Paint事件处理器:

    private void pictureBox1_Paint(object sender, PaintEventArgs e)
    {
        _display.g = e.Graphics;
        _display.Width = pictureBox1.Width;
        _display.Height = pictureBox1.Height;
                
        e.Graphics.Clear(Color.Black);
        if (cbShowResult.Checked)
        {
            _display.PaintValue();
        }
        if (cbShowRoads.Checked)
        {
            _display.PaintRoad();
        }
        if (cbShowParcels.Checked)
        {
            _display.PaintParcel();
        }
        if (cbShowSpot.Checked)
        {
            _display.PaintSpot();
            _display.PaintHover();
            _display.PaintSelect();
        }
        if (!string.IsNullOrEmpty(_display.ToolRuning))
        {
            _display.PaintTool();
        }
    }
    

    可以看到,在事件处理器中分别完成了绘制结果颜色、绘制道路、绘制地块、绘制点状实体、绘制点状实体悬停提示、绘制点状实体选择提示,以及绘制工具功能等模块。其中除了最后一个PaintTool,其他都是原生的静态C#函数,但PaintTool不是,它是一个Action类型的委托变量(Action委托对应不带参数、不返回值的函数)。

    public Action PaintTool = () => { };
    

    PaintTool的作用是为所有的工具进行必要的绘制,我的程序中有很多这样的工具功能,例如地块选择工具,需要根据鼠标位置高亮显示地块;测量工具,根据鼠标点选位置绘制折线;等等。如果使用传统方法,势必需要带参数的函数,例如为了高亮地块,需要传入地块;并且需要使用全局变量来为函数传入参数,因为鼠标位置的获取需要在其他事件处理器中完成。其他绘制函数都是不带参数的,那么这个难道不能统一吗?而且全局变量还是少用为好,函数式编程连变量都反对,何况全局变量。

    让我优雅一次吧。我定义了如下委托变量:

    public Func<CityParcel, Action> PaintHoverParcel;
    

    在SetTool方法中为它赋值:

    PaintHoverParcel = parcel => () =>
    {
        if (parcel != null)
        {
            Point[] pts = parcel.Domain.Points.Select(x => CanvasCoordinate(x)).ToArray();
            g.DrawPolygon(new Pen(Color.Red, 5), pts);
        }
    };
    

    在MouseMove事件处理器中,

    _display.PaintTool = _display.PaintHoverParcel(_display.DetectParcelHover(e.X, e.Y));
    

    其中DetectParcelHover函数用于返回鼠标位置的地块,传入PaintHoverParcel中,返回正好是一个Action,于是赋值给PaintTool。

    为了帮助理解,我做如下解说:为了达成不带参数的绘制地块函数,需要给每个地块做一个单独的绘制函数;但是工作量大,于是用一个函数来实现,那就是一个这样的函数:给定一个地块,返回绘制这个地块的函数。这就是我们讨论的“返回函数的函数”,即高阶函数PaintHoverParcel。

     

    这是我在博客园发表的第一篇文章。写这个随笔只是想呼唤一种精神。我们会发现这样一种现象:在美国,越是从Windows API、MFC、Windows Forms、WPF一路走来的老技术人员,越是为每一次新技术带来的改变而欢呼。同样的事情到了中国,则只有无尽的守旧、抱怨、排斥甚至麻木。园子里时常对LINQ不屑的声音不在少数。我们有足够的理由相信,要想创新,首先需要对新事物有一种宽容,只有宽容对待,才能理性发现、理性领悟、理性升华、理性创造。固步自封、安于现状、畏手畏脚、盲目排新的民族,永远无法以创新者的姿态战胜那些曾经敬仰我们的人。也许,那久违的敬仰已经成为你我每一个人的包袱。

  • 相关阅读:
    catboost原理以及Python代码
    lightgbm原理以及Python代码
    stacking算法原理及代码
    python自动化之爬虫模拟登录
    python自动化之爬虫原理及简单案例
    python自动化之PDF
    input type="file"鼠标无法变小手
    typescript中类和接口的区别
    TypeScript基础类型
    微信小程序-获取input值的两种方法
  • 原文地址:https://www.cnblogs.com/luanshixia/p/1954731.html
Copyright © 2020-2023  润新知