zoukankan      html  css  js  c++  java
  • C#函数式程序设计初探——重构应用篇

    C#函数式程序设计初探——重构应用篇

    篇首语
      在基础理论篇当中已经向大家介绍了Func类、函数闭包及函数柯里化等内容,进而介绍了函数式编程在Linq当中的运用。本文将延续这一话题,继续讨论函数式在重构等方面的一些技巧,希望能对大家的工作带来一些启发。

      本文面向有一定基础的读者,如果在阅读过程中您看不懂某些术语或代码,请移步《C#函数式程序设计初探——理论基础篇》。注意,本文提供的一些思路仅供参考,切勿盲目模仿,否则后果自负。

    主要内容

      利用闭包缓存数据,令方法按需执行,提炼重复参数

    第一部分 利用闭包缓存数据

      首先来看一段简单的示例代码:

    复制代码
    class Program
    {
        static void Main(string[] args)
        {
            int num = 10;
            int result1 = Calculator.Square(10);
            int result2 = Calculator.Square(10);
    
            Console.WriteLine(result1);
            Console.WriteLine(result2);
            Console.ReadKey();
        }
    }
    
    public class Calculator
    {
        private static Dictionary<int, int> SquareResult = new Dictionary<int, int>();
        public static int Square(int x)
        {
            if (!SquareResult.ContainsKey(x))
            {
                Console.WriteLine("缓存计算结果");
                SquareResult[x] = x * x;
            }
            return SquareResult[x];
        }
    }
    复制代码

    Square是一个带有缓存功能的求平方算法,它将计算的结果缓存在了一个词典当中防止重复计算,这个技巧在进行很复杂的计算(比如求正弦)当中是比较有用的(空间换时间)。

      在我们的日常工作当中相信大家都写过或者遇见过类似的代码:一个词典被放置在工厂类当中作为单例容器,也就是所谓的“池模式”。

      这里的求平方只是一个为了说明问题的简单例子,现实需求往往更加复杂,而使用设计模式是需要结合实际需求场景的。设想这样的情景:如果这个计算并不像计算平方这样长期通用,而是希望“缓存词典”中的内容仅仅是在这个方法(算法)的内部多次使用(即离开了Square的调用函数Main,我就想要释放这个词典),那么我就毫无必要为了解决一个算法的时间性能优化的具体问题点,而引入一个新的静态类来污染整个面向对象结构,一方面这样做导致了类数量的膨胀,另一方面调用函数Main与静态类Calculator发生了强耦合调用关系。如果我们的系统中到处都充满了Calculator这样的类,就大大增加了理解、维护和接手的成本。

      在这种情况下,很容易我们就能想到把这个函数定义到调用函数的内部,这个思路和《重构》当中的“提炼方法”是完全相反的(恐怕是因为在Java里没法这么搞),代码如下:

    复制代码
    class Program
    {
        static void Main(string[] args)
        {
            Dictionary<int, int> SquareResult = new Dictionary<int, int>();
    
            Func<int,int> Square = x => {
                if (!SquareResult.ContainsKey(x))
                {
                    Console.WriteLine("缓存计算结果");
                    SquareResult[x] = x * x;
                }
                return SquareResult[x];
            };
    
            int num = 10;
            int result1 = Square(10);
            int result2 = Square(10);
    
            Console.WriteLine(result1);
            Console.WriteLine(result2);
            Console.ReadKey();
        }
    }
    复制代码

    这样一来,我们就从系统当中“干掉”了一个扎眼的静态类,再者,我们发现在后续的调用代码中并没有使用SquareResult这个集合变量,那么我们可以说这个变量同样污染了函数空间,于是乎想到通过柯里化的方式把这个集合移动到Square方法的内部:

    复制代码
    class Program
    {
        static void Main(string[] args)
        {
            Func<Func<int,int>> GetSquareFunc = () => {
                Dictionary<int, int> SquareResult = new Dictionary<int, int>();
                return x => {
                    if (!SquareResult.ContainsKey(x))
                    {
                        Console.WriteLine("缓存计算结果");
                        SquareResult[x] = x * x;
                    }
                    return SquareResult[x];
                };
            };
    
            Func<int,int> Square = GetSquareFunc();
    
            int num = 10;
            int result1 = Square(10);
            int result2 = Square(10);
    
            Console.WriteLine(result1);
            Console.WriteLine(result2);
            Console.ReadKey();
        }
    }
    复制代码

    首先我们定义了一个返回函数的函数起名叫GetSquareFunc,在其中定义了一个词典的局部变量,并在这个函数内部返回一个闭包,这个闭包的内部调用了我们的词典进行缓存和判断。在调用时,我们首先要通过GetSquareFunc来动态生成一个求平方函数,之后使用这个运行时产生的函数来进行求平方操作。

      在这里我们看到了如何利用闭包与柯里化的方式缓存数据,使用了函数式的手段进行代码重构之后我们的世界清静多了,不过有人可能会说这么做有点“反OO”,这不是把算法和调用耦合到一个调用方法里了吗?是的,重构总会有一些副作用,所以说任何重构与模式的使用都是要结合需求情境的。同时也有人会问,你这不是多此一举吗,我干嘛不直接把这个缓存逻辑内联在算法里呢?那么我想问,难道你希望用一堆#region/#endregion让代码成为很长的一坨吗?

       嗯,关于重构的话题已经脱离了本文的范围,而且牵扯到心理学、强迫症、洁癖症等……总之,这是函数式的一个应用,我们还是从需求出发!

    第二部分 令方法按需执行

      首先来看一段代码:

    复制代码
    static void Main(string[] args)
    {bool result = DoSth(2, GetList());
        Console.WriteLine("执行结果" + result);
                
        Console.ReadKey();
    }
    
    static bool DoSth(int x, List<object> list)
    {
        if (x < 10) return false;
        //...
        return true;
    }
    
    static List<object> GetList()
    {
        Console.WriteLine("获取数据源,耗时5秒");
        return new List<object>();
    }
    复制代码

    这段代码有一个容易被我们平常所忽略的诟病,在C#语言中,如果函数的参数是一个函数调用,那么C#一定会先调用这个参数当中的函数,也就是说,如果DoSth的第一个参数小于10,那么获取数据源的5秒钟就白白浪费掉了,而此时我们却又不得不传入一个list作为DoSth的参数!

      显然,这个DoSth的API设计是有问题的!那么我们如何来改造这个方法呢?

      相信大家一定都能想到在DoSth内部来获取list之类的方法,在这里,我们将读取数据的方法作为一个参数传进DoSth当中,并在其内部通过判断后,“惰性”执行读取数据源的方法:

    复制代码
    static void Main(string[] args)
    {
        bool result = DoSth(2, GetList);
        Console.WriteLine("执行结果" + result);
    
        Console.ReadKey();
    }
    
    static bool DoSth(int x, Func<List<object>> GetListFunc)
    {
        if (x < 10) return false;
        List<object> list = GetListFunc();
        //...
        return true;
    }
    复制代码

      这里我们将获取数据源的方法作为一个委托传给了DoSth函数,并在函数内部通过判断后执行这个委托来“延迟”获取数据源,这样一来就解决了获取数据的问题。

      有的人一定会问了,我完全可以在调用这个方法之前定义一个空的集合传进去,判断完成之后再读取数据呀,何苦写一堆Func什么的呢?

      答案是,难道你想为了一个方法定义一个私有的类级别成员吗?如果到处都飘满了这种零散的变量,那还有什么面向对象可言呢?还记得你曾经在aspx.cs后台文件开头写的一堆一堆的变量初始化声明吗?

      这个例子仅仅是演示一下Func作为参数实现延迟调用在重构当中的一个例子,其实这个API设计的真正症结在于它把判断逻辑和业务执行逻辑紧紧耦合在了一个方法里!也就是说要像让这个函数更“纯”一些,就应该把判断逻辑移除到方法之外!

    第三部分 提炼重复参数

      假设在某数据库访问层有这样一个装填参数的辅助类:

    static class SqlParamHelper 
    {
        public static void SetParam(SqlCmd obj, SqlType type, string fieldName, object param);
    }

    以及调用代码:

    SqlCmd sqlCmd = new SqlCmd();
    ParamHelper.SetParam(sqlCmd, SqlType.Guid, "PK", new Guid("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")); ParamHelper.SetParam(sqlCmd, SqlType.String, "Name", "小明"); ParamHelper.SetParam(sqlCmd, SqlType.Int, "Age", 20);

      有没有觉得这个代码很蛋疼呢?我就在工作中见过一些类似这样的例子,以上只是仿造的一段代码。先抛开具体的数据库应用不说,首先这个方法调用有一个共性的参数sqlCmd,从功能实现角度来讲,我不得不传进这个参数才能让SetParam方法做一些有价值的事情,但是每次我都要传进它去,显得实在是太啰嗦了!我们有什么方法来重构这段代码呢?

      核心问题在于,既然SetParam不得不用这个sqlCmd,那么把它提出来了,秉承前面例子的思想,我不想搞一个单独的变量来污染方法空间,那么把它放在哪好呢?

      答案就是使用闭包!

      首先我们定义一个返回函数的方法:

    static Action<SqlType,string,object> GetSetParamFunc(SqlCmd sqlCmd)
    {
        SqlCmd cmd = sqlCmd;
        return (type, fieldName, param) => SqlParamHelper.SetParam(cmd, type, fieldName, param);
    }

    思路是,既然我们要干掉这个参数,那我们既要把它缓存起来,并且返回一个可以利用这个参数的闭包,于是乎,调用的代码就改变成了:

    Action<SqlType,string,object> SetParam = GetSetParamFunc(sqlCmd);
    
    SetParam(SqlType.Guid, "PK", new Guid("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"));
    SetParam(SqlType.String, "Name", "小明");
    SetParam(SqlType.String, "Age", 20);

    首先通过GetSetParamFunc方法的调用,返回一个内部使用sqlCmd值的闭包函数,然后调用这个新获取的函数,使用三个参数来调用。

      另外,你有没有发现这么做之后,方法调用环境和SqlParamHelper静态类的耦合全都被推到了GetSetParamFunc方法之中呢?有没有体会出某些设计模式的味道呢?

    后记

      这篇文章主要介绍了函数式编程在重构当中的运用,实际上函数式编程在科学计算、大数据处理等领域还有更多的应用有待我们学习,希望我的两篇博客能给大家日后涉足这个领域起一个良好的铺垫作用。

     

     

    随着 Windows 8 的推广,以及微软自家平板产品的上市,在这个春季,我们看到了 Win8 平板、变形本的百花齐放。各大 OEM 厂商也都一改传统笔记本的设计,将多款 Win8 触控变形超极本逐步推向市场。在接下来的数月内,各大 OEM 厂商还将继续推出面对不同消费需求的新设备,我们看到了一个 PC+ 时代的到来。

    在 PC+ 时代,我们可以使用各种端设备与云数据中心保持连接,从智能手机到平板电脑,从传统的 PC 机到变形本,从 Xbox 游戏机到多点触控的大电视,人们开始采用不同的智能设备满足不同场合的需求,为生活和工作增添便捷和精彩。我们在购买新型设备时,不能再用传统笔记本电脑的概念去衡量和思考,而是应该根据实际需求去购买比较符合使用场景的新设备。例如, Windows RT 设备的处理能力虽然没有 Windows 8 x86/x64 设备那么强劲,但是它们往往很轻薄,可以做得很小,而且电池非常耐用,价格也会相对便宜,是用于上网、娱乐和移动使用的最佳设备,同时还可以通过内置的 Office 软件创作文档、满足一些商务需求。

    但对于只想购买一台设备的用户而言,带有平板功能的超极本无疑是一个不错的选择,于是,变形本就应运而生了。它既有平板电脑的使用方式,也有传统笔记本电脑的用法,通过硬件设计上的创新,来达到变形的效果。在众多的变形本中,我个人还是很喜欢索尼的 Duo 11,它简于形却精于心,是我认为的一款相对完美的变形本。

    Sony Duo 11 开箱后,是一个平板的模样,四四方方,却有着赏心悦目的带有弧度的边角。它有着11.6寸的屏幕,分辨率达到了 1920x1080 的全高清显示。用手托起觉得轻重适中,显示效果很是细腻。在外出时或者躺在床上时,当个平板用足矣。平板的形态下,它显得很简单,很大方。

    说它精于心,其中一个因素是其通过在有限空间里融入的巧妙设计,不用拆卸组合屏幕,就能变身为笔记本电脑的模样;另一个因素是,它的细节为用户考虑得很到位,实用性很强,仅仅靠它全面的硬件接口就能说明这一点了。我们可以来看看具体的细节。

    Duo 11 采用的是类似滑盖的设计,里面采用的弹簧和背板支撑结构,能让用户在抬高屏幕上方中部时,轻松地将屏幕掀起,并保持良好的视角将屏幕支撑牢固。与此同时,原本藏在屏幕底下的实体键盘也就显露出来,这样打起字来就会方便许多。巧克力键盘的触感总体来说还不错,并且键盘还有白色的背光,显得很优雅,在夜间也会很实用。键盘中部有一个光学指点杆,可以实现鼠标指针的移动。同样值得惊喜的是,与指点杆配套的鼠标按键不是两个,而是三个,顿时觉得它的定位也高端了许多。

    在接口方面,它有两个 USB 3.0 接口,其中有一个可以在关机时为手机等 USB 连接的设备供电。它有了一个 HDMI 接口的同时,还提供了一个全尺寸的 VGA 接口,同时还提供了一个多合一读卡器和一个 RJ-45 网口。要知道,现在很多办公室的投影仪默认就只留了一个 VGA 线供笔记本连接,而且,在外面的很多旅馆里,用网线上网也是常有的事儿。在很多 OEM 厂商将 VGA 和 RJ-45 从机身上省去的时候,索尼为商务人士提供了多种选择,让用户在出差上网和外出演示的时候与周遭设备最大兼容,从而无需携带额外的转换头或者转换线缆。此外,它还有3.5mm耳机孔,和贴心的屏幕旋转锁定、音量控制等按钮。通过紧凑的工业设计,这些个大大小小的接口,不该多的不多,不该少的没少,全都恰当地排列在了机身的周围,可谓良心品质。

    还有一个可以体会其精于心的地方,得说说随机附赠的数字笔。这款数字笔是金属材质的,非常有质感,而且为了不同的书写体验,连笔头都有两支,一支是类似塑料的材质,还有一支是金属材质的。不同的笔头也可以带来不同的触感。有了数字笔,用户可以方便的做笔记,或者使用数字墨水技术,在 Office 文档里进行墨迹批注。

    在传感器方面,它不仅带有常规的传感器集合,还提供了 NFC 芯片。有了 NFC,在设备与设备间的近距离数据交换时,是非常方便的。

    这款设备总体而言是非常出色的,我个人也是怀着激动的心情好好地把玩了几天。不过,美中不足的是,电池的续航能力只有约3~4小时(貌似官网可以选配附加电池),并且数字笔的压感级别只有256级。不过,i7 CPU + 8 GB RAM,耗电是可以理解的,而对于数字笔而言,可能索尼的定位在于普通家用或者商务办公使用,而不是通过笔提供专业的绘画体验。

    这次同样录制了一个上手体验视频,如果您感兴趣的话可以看看:




     
    华煜 (Eric Sheh),微软 MVP,致力于为微软 Windows 和 Office 用户及 IT 专业人士提供更好的帮助与支持。 关注我的 新浪微博,加入 我要做电脑达人 微群,收听每日 Office 效率提升秘籍~ 

        点此了解作者介绍及最新的预约演讲、会议、文章发布等信息

     

     

     

     

  • 相关阅读:
    C语言的指针移位问题
    makefile、gdb使用记录
    MUSIC算法学习笔记
    时间格式转换
    linux网络编程笔记——UDP
    linux网络编程笔记——TCP
    bash里,echo对换行符的处理
    树莓派使用8188eu无线网卡
    时间都去哪了?——安卓GTD工具
    让sublime text 2更好地支持Python
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3037711.html
Copyright © 2011-2022 走看看