C#高阶函数介绍

导语

一般常用的高阶函数函数有Map,Filter,Fold,Flatten,FlatMap。C#的函数式编程一般用它自带的LINQ,LINQ我猜想它是从数据库SQL语言的角度出发的。所以命名有些不一样。

  • Map,对应C#的Select
  • Filter,对应C#的Where
  • Fold,对应C#的Aggregate

个人来讲还是比较喜欢Map,Filter,Fold原来的这些名字,用过Lisp,Scala,Haskell的人一看就明白这些是什么意思了。但是既然C#自带提供了,那我们就直接使用吧

Select(Map)

先看一个例子,既然C#取名Select,那我们先举一个类似数据库的例子把。

struct People 
{
    public string Name { get; set; }
    public int Age { get; set; }
}
static void test1() 
{
    People[] PeopleList = 
    {                   
        new People { Name="A", Age = 1}, 
        new People { Name="B", Age = 2}, 
        new People { Name="C", Age = 3},       
    };
    PeopleList.Select(it => it.Name).ToList().ForEach(it =>
    {
        Console.WriteLine(string.Format("NAME:{0}", it));
    });
    PeopleList.Select(it => it.Age).ToList().ForEach(it =>
    {
        Console.WriteLine(string.Format("AGE:{0}", it));
    });
}
=====运行结果=====
NAME:A
NAME:B
NAME:C
AGE:1
AGE:2
AGE:3
==================

以上例子可以看出Select函数把People里面属性提取出来,但是Select的功能,远大于此。其他很多语言其实叫Map,其实我更喜欢Map这个名字。Map更能贴切的形容这个功能,我们应该 把它理解为数学概念上的集合上映射。

映射

请看下面的例子

static void test2() 
{
    int[] ilist = { 1, 2, 3, 4, 5 };
    ilist.Select(it => it * 2).Select(it => it.ToString()).ToList().ForEach(it => 
    {
        Console.WriteLine(it);
    });
}
=====运行结果=====
2
4
6
8
10
==================

这个例子可以看出,原来的list的元素全部映射为原来的两倍。其实我们可以用Select做更复杂的映射。我们先定义一个新的类型Student。

struct Student
{
    public string Name { set; get; }
    public int Age { set; get; }
}
static void test3() 
{
    People[] PeopleList = 
    {                   
        new People { Name="A", Age = 1}, 
        new People { Name="B", Age = 2}, 
        new People { Name="C", Age = 3},       
    };
    PeopleList.Select(it => new Student
    {
        Name = "Student" + it.Name,
        Age = it.Age,
    }).ToList().ForEach(it => 
    {
        Console.WriteLine(string.Format("NAME:{0} AGE:{1}",it.Name,it.Age));
    });
}
=====运行结果=====
NAME:StudentA AGE:1
NAME:StudentB AGE:2
NAME:StudentC AGE:3
==================

如上面的例子我们把原来为People的数据类型映射为Student了。

Where(Filter)

Where像是集合的减法,过滤掉不符合条件的数据。
假设我们接到一个需求,把大于等于5岁小于15岁的人抓起来送去学校当学生。可以像以下这么写。

static void test4() 
{
    People[] PeopleList = 
    {                   
        new People { Name="A", Age = 1}, 
        new People { Name="B", Age = 2}, 
        new People { Name="C", Age = 5},       
        new People { Name="D", Age = 6},  
        new People { Name="E", Age = 7},  
        new People { Name="F", Age = 10},
        new People { Name="G", Age = 20},
        new People { Name="H", Age = 21},
    };
    PeopleList.Where(it => it.Age >= 5 && it.Age < 15).Select(it => new Student
    {
        Name = it.Name,
        Age = it.Age
    }).ToList().ForEach(it => 
    {
        Console.WriteLine(string.Format("NAME:{0} AGE:{1}", it.Name, it.Age));
    });
}
=====运行结果=====
NAME:C AGE:5
NAME:D AGE:6
NAME:E AGE:7
NAME:F AGE:10
==================

Fold

下面开始介绍Fold,C#里面有一个函数Aggregate,和此功能类似。但是我实在是受不了这个名字,我自己写了一个,如下。按照C#的扩展方法来写的,这样的话我就可以直接在原来是数据类型中使用了。

public static R FoldL<T, R>(this IEnumerable<T> list, Func<R, T, R> accmulator, R startValue)
{
    R v = startValue;
    foreach (T item in list)
    {
        v = accmulator(v, item);
    }
    return v;
}  

FoldL是左折叠的意思,把一串数据,从左边开始累加在一起,至于用什么方式累加那就看accmulator函数了。startValue是初始值。有左折叠,当然就有右折叠,但是右折叠我一般不会用到。请看下面的例子。

static void test5()
{
    int[] ilist = { 1,2,3,4,5,6,7,8,9,10};
    Console.WriteLine(ilist.FoldL((acc, it) => acc + it, 0));
    Console.WriteLine(ilist.FoldL((acc, it) => acc + it + ",", "").TrimEnd(','));
}  
=====运行结果=====
55
1,2,3,4,5,6,7,8,9,10
==================

这个例子用了两种累加的方法,第一种是初始值是0,然后从左边直接相加。
第二种是初始值是""空字符串,从左边开始,先把数据转化为字符串,在累加之前的字符串且在后面加上逗号。最终的结果会多出一个逗号,所以我在最后加了TrimEnd(',')去掉最后的逗号,让数据更好看点。

static void test6()
{
    Student[] StudentList = 
    {
        new Student { Name="A",Age=10},
        new Student { Name="B",Age=11},
        new Student { Name="C",Age=10},
        new Student { Name="D",Age=13},
    };
    Console.WriteLine(StudentList.FoldL((acc, it) => acc + it.Age, 0) / StudentList.Length);
}  
=====运行结果=====
11
==================

这个例子可以求出所有学生的平均年龄,虽然C#有自带的SUM函数,但是我在这里还是用自己的FoldL函数来实现。

Flatten

Flatten函数也是很常用的,目的是把IEnumerable<IEnumerable<T>>两层的数据变成一层IEnumerable<T>数据。这个对嵌套结构类型的数据处理非常有用。在C#我好像没有找到类似的,所以我自己写了一个。Flatten英文意思是变平,我们能形象地理解这个函数的意思是把两层IEnumerable变成一层IEnumerable,当然它可以把三层变成两层。Flatten的意思是把数据变平一点点,它的参数至少是接受两层的数据。

public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> list)
{
    foreach (IEnumerable<T> item in list)
    {
        foreach (T it in item)
        {
            yield return it;
        }
    }
}  

如下面的例子,我们把Student这个类型添加一个课程的属性,一个学生可能选择多门课程。假设我的接到的需求是把学生选的课程和学生的名字打出一张表出来。请看下面的例子。

struct Student
{
    public string Name { set; get; }
    public int Age { set; get; }
    //课程
    public List<string> Courses { get; set; }
}  
static void test7()
{
    Student[] StudentList =
    {
        new Student { Name="A",Age=10,Courses=new List<string> { "数学","英语"}},
        new Student { Name="B",Age=11,Courses=new List<string> { "语文"}},
        new Student { Name="C",Age=10,Courses=new List<string> { "数学","生物"}},
        new Student { Name="D",Age=13,Courses=new List<string> { "物理","化学"}},
    };
    StudentList.Select(it => it.Courses.Select(course => new
    {
        Name=it.Name,
        Course=course
    })).Flatten().ToList().ForEach(it=> 
    {
        Console.WriteLine(string.Format("Name:{0} Course:{1}",it.Name,it.Course));
    });
}  
=====运行结果=====
Name:A Course:数学
Name:A Course:英语
Name:B Course:语文
Name:C Course:数学
Name:C Course:生物
Name:D Course:物理
Name:D Course:化学
==================

这个例子中,我们还用到了C#的匿名类型。

FlatMap

这个函数其实可以理解为把数据先做Flatten再做一次Map。例子就不写了,代码贴这里

public static IEnumerable<R> FlatMap<T, R>(this IEnumerable<IEnumerable<T>> list, Func<T, R> convert)
{
    foreach (IEnumerable<T> item in list)
    {
        foreach (T it in item)
        {
            yield return convert(it);
        }
    }
}  

ForEach

C#只提供了List类型的ForEach函数,但是没有提供IEnumerable类型的ForEach,我们自己写一个。代码如下

public static void ForEach<T>(this IEnumerable<T> list, Action<T> action)
{
    foreach (T item in list)
    {
        action(item);
    }
}  

GroupBy

最后介绍下C#的这个GroupBy函数,非常有用。如下面例子。

struct Student
{
    public string Name { set; get; }
    public int Age { set; get; }
    //课程
    public List<string> Courses { get; set; }
    public int Sex { get; set; }
    public int Class { get; set; }
}  

我们在Student类型里面添加多两个属性。性别Sex属性(男0,女1),班级属性Class。
需求1:把女生全部找出来
需求2:把一班的所有女生找出来

static void test8()
{
    Student[] StudentList =
    {
        new Student { Name="A",Age=10,Sex=1,Class=1,Courses=new List<string> { "数学","英语"}},
        new Student { Name="B",Age=11,Sex=0,Class=2,Courses=new List<string> { "语文"}},
        new Student { Name="C",Age=10,Sex=1,Class=1,Courses=new List<string> { "数学","生物"}},
        new Student { Name="D",Age=13,Sex=1,Class=2,Courses=new List<string> { "物理","化学"}},
    };
    StudentList.GroupBy(it => it.Sex).Where(it=>it.Key==1).ForEach(it => 
    {
        it.ForEach(student => 
        {
            Console.WriteLine("NAME:" + student.Name);
        });
    });
    StudentList.GroupBy(it => new { SEX = it.Sex, CLASS = it.Class })
        .Where(it => it.Key.SEX == 1 && it.Key.CLASS == 1).Flatten().ForEach(it=> 
        {
            Console.WriteLine("一班女生名字:"+it.Name);
        });
}  
=====运行结果=====
NAME:A
NAME:C
NAME:D
一班女生名字:A
一班女生名字:C
==================

上面的这个例子我直接把ForEach用上了,而且在需求2中我把Flatten用上了,这样我就不需要用两次ForEach了。

其他

我们用了这些高阶函数之后,基本上一个For循环都不用再写了。需要注意的是C#的这些函数都是惰性调用的,它们是用IEnumerable的特性来实现惰性调用的。这些高阶函数求出来的值,在需要的时候才会真正的执行循环体的调用。有很多细节需要理解,需要注意,后续我会详细的举例子来说明这些细节。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容