C#函数式程序设计初探——基础理论篇

篇首语

  近来发现园子里有不少人在讨论函数式相关的问题,从个人性格来讲,我不爱看学术气氛太强的东西,从责任上来讲,我认为也有必要写一篇“干货”把函数式这个问题说得明白一些,也作为自己的一个知识沉淀,于是便有了此文。

  个人认为,C#语言的某些设计并不非常适合函数式开发,比如它的类型推断并不是很近乎人意,我们知道C#还是主打面向对象的,不过这并不妨碍我们用C#来讨论函数式,至少可以借鉴函数式的一些思路来优化我们的代码。

  我希望通过这篇文章让读者通过简单的例子,在短时间内掌握基本函数式编程方法,了解Action与Func类型的使用。同时我希望读者对C#泛型集合、Linq、lambda表达式和yield关键字有所了解。

 

主要内容

  Action与Func类型介绍,在函数内部定义函数与返回函数,闭包与函数柯里化,高阶函数与Linq应用。

 

第一部分 Action与Func类型介绍

  近来有一些人问我Action和Func类型是什么意思,为了整篇文章知识体系的完整性,先来给大家做一番介绍(如果你熟悉这两个类型,请跳过这部分)。

  首先来看这样一个JavaScript函数:

function sum(n1, n2) {
    return n1 + n2;
}

我们知道,在JavaScript当中,函数是可以赋值为一个变量的,即:

var sum = function(n1, n2) {
    return n1 + n2;
}

定义这个“变量”之后,我们可以通过sum(1,2)的方式调用这个函数。那么,如果javaScript是一种强类型语言的话,这个var是什么类型呢?

来看一下这个函数的C#代码:

static int Sum(int n1, int n2)
{
    return n1 + n2;
}

注意到这个函数接收了两个int型参数,返回了一个int值。那么,它的类型就是Func<int,int,int>,即它的等效代码为:

Func<int,int,int> Sum = (int n1, int n2) => {
    return n1 + n2;
};

我们可以F12一下,看到Func类的定义如下:

public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);

  这个类型实质上是一个委托,返回值是个泛型的TResult,从定义的参数表可以看出,前两个类型T1和T2是传入参数的类型,第三个类型是返回值类型。

  根据这个道理,假设有一个Func<int,string,bool>型的变量,它表示一个委托,这个委托内包含了这样一个函数:该函数的两个参数是int和string类型,返回值为bool。当Func<TResult>只有一个类型参数时,TResult表示返回值类型,即Func<bool>表示一个委托,它的参数表为空,返回值为bool类型。为了方便说明,下文将委托与函数两个概念通通使用“函数”来表示。

  猜猜看Func<object, Func<string,bool>>表示什么呢?它表示一个函数,接受一个object类型的参数,返回一个Func<string,bool>。这里可以看出,函数也是可以作为函数的返回值的。

  接下来看Action,我们F12一下看Action<T>的定义:

public delegate void Action<in T>(T obj);

注意到委托的返回值为void,那么实际上Action就是一个没有返回值,只有参数表的委托,即Action<T1,T2>等价于Func<T1,T2,void>。

  最后来说一下Predicate<T>,当我们写Linq方法的Where()时,可以看到它要求传入了一个Predicate类型的参数,它实际上就是一个bool型委托,等价于Func<T,bool>。

  这里只是对这几个委托关键字做一个铺垫性的介绍,大家可以去网上搜这两个关键字的用法相关的帖子,如果没搞懂请不要往下看。 

 

第二部分 在函数内部定义函数与返回函数

  那么有人该问了,好好的一个函数,干嘛非写成Func这样蹩脚的形式呢?下面来看一个例子:

static void DoSth()
{
    //前置逻辑
    if (Validate())
    { 
        //后续逻辑
    }
}

static bool Validate()
{
    //校验逻辑
    return true;
}

  也许这个例子不够恰当,但是足以说明问题,我想略有经验的程序员都明白将校验方法(或者说,比较方法,嗯)重构到一个新函数里,这样能让程序脉络清晰,《重构》当中也提到了这一手段,但是有没有意识到这种做法有一个诟病:这两个方法处于一个类环境当中,通常来说DoSth方法是publish的,那么为了重构,我们不得不在这个类环境当中搞出一个private方法来支撑这个public方法,显然这个private方法没有什么可复用性可言,而且它污染了整个类空间,再说从面向对象的角度来看,校验成了我这个类要承担的职责,这岂不是很诡异?

  那么我要做的,就是在提取这个Validate方法的前提下,保证这个方法别污染类空间。那么一个切实可行的办法,就是把这个校验函数定义在DoSth的内部,代码如下:

static void DoSth()
{
    Func<bool> Validate = () => {
        //校验逻辑
        return true;
    };

    //前置逻辑
    if (Validate())
    { 
        //后续逻辑
    }
}

  这段代码把校验函数定义为了DoSth内部的一个变量,它的生存期就在DoSth内部,这样一来就丝毫不会影响类的结构了。这就是Func的应用之一——在函数内部定义局部函数。

  但是这样还是让人觉得很啰嗦,这个Validate完全可以在其他地方定义,然后作为参数传进来,比如这样:

static void DoSth(Func<bool> Validate)
{
    //前置逻辑
    if (Validate())
    {
        //后续逻辑
    }
}

如此一来,这个校验方法就可以定义在其他地方了,这就给我们做一些面向对象方面的方便(比如通过依赖注入搞到这个函数),当然也可以在调用的时候直接在参数里写lambda表达式:

static void Main(string[] args)
{
    DoSth(() => { 
        //校验逻辑
        return true;
    });
}

  可能这个“校验”的例子举得不是很恰当,但是这已经足够说明Func作为参数的用法。

  如果你怀疑这种手段的实际价值,想想JavaScript里的SetTimeout的第二个参数吧!所谓的回调函数,就是一种由框架调用由客户端实现的函数,用这种写法可以大大增加客户端代码的直观性与灵活性!

  既然Func类型可以作为函数的参数,那么它可不可以作为函数返回值呢?答案必然是肯定的,我们还是来看一个加法例子:

static Func<int, Func<int, int>> Sum = n1 => {
    return n2 => n1 + n2;
};

观察返回值类型Func<int, Func<int,int>>,它表示这个函数接受一个int型参数,返回一个Func<int,int>,也就是返回一个接受int类型参数,返回int类型值的函数。即,Sum是一个返回函数的函数。

那么这个函数如何使用呢?观察下列主函数:

static void Main(string[] args)
{
    var Sum5 = Sum(5);
    int result = Sum5(10);

    Console.WriteLine(result);
    Console.ReadKey();
}

首先,我们通过Sum(5)的方式,返回了一个Sum5变量,这个变量的类型是Func<int,int>,也就是说,我们通过Sum函数返回了Sum5函数。接下来调用这个新函数Sum5(10),得到了答案15。当然,接下来我还可以调用Sum5(20)得到25。

  自然地,这个调用可以写成Sum(5)(10),与原本的Sum(5,10)相比,新的写法将两个参数拆解到了多个括号之中分部调用。聪明的你一定能发现这么做的好处,就是把这个参数解耦,让各个算法(函数)之间有更高的灵活性和可复用性。但是要注意的是,要得到最终的结果,参数的数量依旧是一个都不能少的。

  另外,你有没有从这里嗅出一些“重载”的味道?

 

第三部分 闭包与函数柯里化

  不要被这个标题吓倒,嗯!我们来改写一下刚才的代码:

static void Main(string[] args)
{
    var Sum5 = Sum();
    int result = Sum5(10);

    Console.WriteLine(result);
    Console.ReadKey();
}

static Func<Func<int, int>> Sum = () => {
    int n1 = 5;
    return n2 => n1 + n2;
};

这次我们让Sum不再接收第一个参数了,而把n1定义在Sum方法的内部,调用就变成了Sum()(10),大家可以试一下,结果依旧输出15,一切看似很自然,不过请你反复读一读Sum的定义,是不敢觉得似乎少了点什么?希望你停下来多读几遍再往下看!

问题就出在n1的定义,请回答一个问题,变量n1的生存范围是多大?Sum函数返回的时候,n1既然是Sum的内部的局部变量,应该就被释放掉了,那么我调用Sum5(10)的时候,被释放掉的5是从哪里来的呢?

在解释这个问题之前,我想你应该可以理解“Func<Func<int,int>> Sum = xxx”这种写法,等价于“Func<int,int> Sum() { xxx }”,如果不理解,请停下来,把上面的部分再看一遍。

我们打开反编译器对这个Sum的定义,可以看到:

[CompilerGenerated]
private static Func<int, int> <.cctor>b__0()
{
    <>c__DisplayClass3 CS$<>8__locals4;
    return new Func<int, int>(CS$<>8__locals4, (IntPtr) this.<.cctor>b__1);
}

奇怪的是,在这个函数的第一句话,定义了一个“<>c__DisplayClass3”匿名类的对象,也就是说,Sum5这个函数的内部携带着这个对象,想必5这个数字就保存在这个类里,来看这个类的定义:

[CompilerGenerated]
private sealed class <>c__DisplayClass3
{
    public int n1;

    public int <.cctor>b__1(int n2)
    {
        return (this.n1 + n2);
    }
}

看到这里我想我不用再解释什么了吧。

  观察我们的函数n2=>n1+n2,它能够拿到外部函数Sum中的n1,而Sum却不能拿到它内部的n2,这一类的函数,起个名字——闭包。于是现在你稍微理解JavaScript中那个叫作用域链的东西了吗?

  嗯,这部分的标题上提到了函数的柯里化,那什么是柯里化呢?其实刚才已经看过了,把Sum(5,10,15,20)写成Sum(5)(10)(15)(20)就叫柯里化,或者说把Func<int,int,int>搞成Func<int, Func<int,int>>就叫柯里化,也是起个名字唬人的,就像“面向切面编程”这个名字一样!

 

第四部分 高阶函数与Linq应用

  现在进入理论篇的最后一部分,神马叫高阶函数?还就是起个名字而已,以其他函数做参数、或者返回一个函数的函数,就叫高阶函数,刚才的Sum就是高阶函数。至此大家已经了解了如何在函数中调用一个作为参数的函数,为了给后面的应用篇做铺垫,这里介绍几个经典的高阶函数,希望大家都能理解。

  (1)Map函数:接受一个转换函数和一个集合,对这个集合中的每个元素,延迟返回它执行转换函数后的值。

static IEnumerable<TR> Map<T, TR>(Converter<T, TR> select, IEnumerable<T> list)
{
    foreach (T val in list)
    {
        yield return select(val);
    }
}

其中Converter是一个委托,它接受一种类型的参数,返回另一种类型的参数,也就是说如果有一个Converter类型的函数,其作用就是将一种类型转换为另一种类型,当然,在使用的时候,我们可以传递一个很复杂的类,返回其中的某个字段。

public delegate TOutput Converter<in TInput, out TOutput>(TInput input);

  (2)Filter函数:接受一个布尔函数作为判断条件,作用在一个集合上,延迟返回这个集合当中满足条件的元素。

static IEnumerable<T> Filter<T>(Predicate<T> selector, IEnumerable<T> list)
{
    foreach (T val in list)
    {
        if (selector(val))
        {
            yield return val;
        }
    }
}

  (3)Fold函数:接受一个返回TR类型的算法函数,一个TR类型的起始值,及一个集合,对这个集合中的所有值应用这一算法,并”折叠“到返回值上返回。

static TR Fold<T, TR>(Func<TR, T, TR> accumulator, TR startVal, IEnumerable<T> list)
{
    TR result = startVal;
    foreach (T val in list)
    {
        result = accumulator(result, val);
    }
    return result;
}

  大家有没有看出这三个函数有什么猫腻?它们都有一个IEnumerable<T>的参数,那么下面我们就把他们改造为扩展方法,并且改个名:

public static partial class Enumerable
{
    public static IEnumerable<TR> Select<T, TR>(this IEnumerable<T> list, Converter<T, TR> selectField)
    {
        foreach (T val in list)
        {
            yield return selectField(val);
        }
    }

    public static IEnumerable<T> Where<T>(this IEnumerable<T> list, Predicate<T> selector)
    {
        foreach (T val in list)
        {
            if (selector(val))
            {
                yield return val;
            }
        }
    }

    public static TR Sum<T, TR>(this IEnumerable<T> list, Func<TR, T, TR> accumulator, TR startVal)
    {
        TR result = startVal;
        foreach (T val in list)
        {
            result = accumulator(result, val);
        }
        return result;
    }
}

我们可以这样使用:

static void Main(string[] args)
{
    IEnumerable<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8 };

    list.Where(num => num % 2 == 0)
        .Select(num => num)
        .ToList().ForEach(num => {  //这里就直接调用Linq了
            Console.WriteLine(num);
        });

    int sum = list.Where(num => num % 2 == 0)
        .Sum((x, y) => x + y, 0);
    Console.WriteLine("sum=" + sum);

    Console.ReadKey();
}

这基本和Linq没有什么差别了,嗯,其实Linq里就是这么搞的,只是它更加丰富和严谨,依旧不用多解释了。

 

后记

  相信大家读完这篇文章之后已经对函数式编程有了一个初步的认识,函数式还有很多精彩的应用,请关注下回分解!

posted @ 2013-04-20 23:52  以利亚  阅读(2630)  评论(16编辑  收藏  举报