《高效编程十八式》(2/13)数据统计:泛型与委托

来源:互联网 发布:淘宝pos机突然不能买了 编辑:程序博客网 时间:2024/06/06 21:43
 

数据统计:泛型与委托

王伟冰

    我们常常会遇到这样的问题,比如说,统计一个班的学生中数学成绩大于60分的人数。假如说所有学生的成绩储存在一个int型数组中,那么我们可以定义这样的函数:

    int count(int scores[],int n){

        int m=0;

        for(int i=0;i<n;i++)

            if(scores[i]>60)m++;

        return m;

    }

    其中scores中储存学生成绩的数组,n是数组的长度。

    于是我们就可以用诸如count(a,30)这样的代码来统计一个30人的班上的及格人数了。

    但是,这样的一个函数,限死了只能统计大于60分的人数,不能统计大于70分、80分的人数,所以我们可以把函数改成这样:

    int count(int scores[],int n,int min){

        int m=0;

        for(int i=0;i<n;i++)

            if(scores[i]>min)m++;

        return m;

    }

    这样不就提高了灵活性了吗?所以,对于有可能改变的数值,不要写死,可以作为函数参数传进来。(灵活原则1)

 

    但是可能过几天之后,你发现有的学生的成绩不是整数,可惜你的这个函数只能处理整数的情况,只好另写一个:

    double count(double scores[],int n,double min){

        ……

    麻不麻烦?其实,如果从一开始就预料到类型可能会发生改变,那么不妨把函数定义成模板:

    template<class T>

    int count(T scores[],int n,T min){

        int m=0;

        for(int i=0;i<n;i++)

            if(scores[i]>min)m++;

        return m;

    }

    其中T称为模板参数,如果T是int,那么就得到前面int版本的count函数;如果T是double,就得到前面double版本的count函数。比如这样调用:

    m=count<int>(a,30,60);//调用int版本

    m=count<double>(b,40,60);//调用double型版本

    m=count(c,30,60);//编译器会根据c的类型自动推断出用哪个版本

    所以,对于有可能改变的类型,不要写死,可以写成模板,把类型作为模板参数传进来。包括类模板和函数模板。(灵活原则2)这就是 “泛型编程”。而且这样也体现了简洁原则,因为不需要对每种类型分别写一个单独的版本。

 

    但是,问题还没有完,有时我们不仅需要统计成绩大于某个值的人数,还要统计小于某个值的人数,或者是介于某两个值之间的人数。考虑到要尽量利用已有的类和函数来构造新的功能,而不改变类和函数的内部代码。(灵活原则3)你可以这样子来实现:

    m=30-count(a,30,60);//总人数减去大于60的人数得到小于60的人数

    m=count(a,30,60)-count(a,30,70);//大于60的人数减去大于70的人数得到介于60和70之间的人数

    尽管这种方法很巧妙,在很多情况下是一种实用的思路,但是这毕竟是一种治标不治本的做法(而且性能不好),假如我要你统计出成绩是奇数或偶数的人数呢?无能为力了吧。根本的方法是使用函数指针:

    template<class T>

    int count(T scores[],int n,bool func(T)){//func代表一个函数指针

        int m=0;

        for(int i=0;i<n;i++)

            if( func(scores[i]) )m++;

        return m;

    }

    bool func1(double x){//判断x是否大于60

        return x>60;

    }

    bool func2(int x){//判断x是否是偶数

        return x%2==0;

    }

    这样你就可以这样子做统计:

    m=count(a,30, func1);//统计分数大于60的人数

    m=count(a,30, func2);//统计分数是偶数的人数

    其中bool func(T)表示一个名为func的函数指针,它指向一个函数,这个函数的参数为T型,返回值为bool型。当你把func1(或func2)作为参数传给count函数时,func指针就指向了func1(或func2)函数。当执行到func(scores[i])时,实际上就执行了func1(scores[i])(或func2(scores[i]))。

所以,当一个函数里有一段有可能改变的代码时,不要写死,可以把这段代码委托给一个传进来的函数指针去执行。(灵活原则4)这种思想就叫做“委托”。

 

 

    回顾前面的灵活原则1,我们用一个min参数来代替60,从而提高了灵活性和简洁性。我们希望func1函数也能做到这一点,能够判断x是否大于一个设定的数值min.于是对它进行了改造:

    bool func1(double x,double min){

        return x>min;

    }

    这样做对吗?那调用count怎么调用?count(a,30,func(,60))???

    没有任何语言支持这种语法,count要求传入的函数指针必须是bool func(double)型的,也就是接受一个double型参数并返回bool型的,所以func1不能接受两个参数。解决方法可以是这样:

    double min;

    bool func1(double x){

        return x>min;

    }

    我们可以在调用count之前先设定min的值:

    min=60;

    m=count(a,30,func1);

    可惜的是,min变量变成了一个全局变量,所有的函数都可以访问它,通常我们要严格控制变量作用域,只有真正需要用到某个变量的代码段才可以访问到这个变量。(清晰原则2)你可能觉得这没什么大不了,但是在大项目里,过多不必要的全局变量可能会导致不经意的命名冲突,使得在别的地方对另外一个变量的访问不小心变成了对这个变量的访问。即使不存在任何同名现象,在多线程的程序里对全局变量的异步访问也会导致意想不到的结果,这个后面还会说到。

 

    更好的实现应该使用函数对象,我们需要更改count和func1的定义:

    template<class T,class T1>

    int count(T scores[],int n,T1 func){//func的类型未定

        int m=0;

        for(int i=0;i<n;i++)

            if( func(scores[i]) )m++;

        return m;

    }

    class Func1{

        int min;

    public:

        Func1(int x){//构造函数

            min=x;

        }

        bool operator()(double x){//重载()运算符,判断x是否大于min

            return x>min;

        }

    }

    于是就可以这样子:

    m=count(a,30,Func1(60));//统计分数大于60的人数

    怎么这么复杂?这个过程发生了什么事?或许这样写会清楚一些:

    Func1 func1(60);

    m=count<double,Func1>(a,30,func1);

    首先,Func1是一个类型,而不是一个函数。Func1 func1(60)调用了Func1类的构造函数,新建了一个Func1型的对象func1,并把它的成员变量min设为60。然后把func1传给count函数的第三个参数,即T1 func。所以T1就是Func1,func就是func1。

    当执行func(scores[i])的时候,就相当于执行func1(scores[i]),即调用了Func1类的()运算符,把score[i]作为参数x传入,返回x>min即score[i]>60的值。在这里,func1对象就叫做函数对象,因为它本质是一个对象,却表现得像一个函数指针。

    太麻烦了。为了增加灵活性得写这么长一个Func1类。有没有简单一点的办法?有。可以利用C++内置的函数对象(需要#include <functional>):

    m=count(a,30,bind2nd(greater<double>(),60));

    这样就不用写Func1类了。但是这个实在很难看懂,我也不打算在这里解释它的意思……你猜,如果要统计60到70间的人数该怎么写?

    m=count(a,30,logical_and(bind2nd(greater<double>(),60),bind2nd(less_equal<double>(),70)));

    也许有的人会觉得这个很酷,但是你看一下C#是怎么写的:

    m=a.Count((x)=> x>60 && x<=70);

    简洁吧。“(x)=> x>60 && x<=70”这个东西叫做“lambda表达式”,代表一个匿名的函数,前面的“(x)”表示这个函数有一个参数x,“=>”是lambda表达式的标志,后面表示函数返回x>60 && x<=70的计算结果。上面的写法是简化的写法,完整的是:

    m=a.Count((double x)=>{ return x>60 && x<=70; })

    所以lambda表达式是一个非常有用的东西,我认为现代的编程语言都应该包含lambda表达式。事实上,C++新标准也包含了lambda表达式,你可以这样写:

    m=count(a,30,[](double x){ return x>60 && x<=70; });

    可以看到,和C#的完整写法其实大同小异,只是“=>”变成了“[]”,并且提到前面而已。总之,如果需要对函数指针进行更加灵活的定制,可以使用函数对象或者lambda表达式。(灵活原则5)

原创粉丝点击