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 ==================
请看下面的例子
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的元素全部映射为原来的两倍。
这个例子可以看出,原来的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 ==================
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; } } }
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,相当于c#中的SelectMany。例子就不写了,代码贴这里
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的特性来实现惰性调用的。这些高阶函数求出来的值,在需要的时候才会真正的执行循环体的调用。有很多细节需要理解,需要注意,后续我会详细的举例子来说明这些细节。
************转摘:https://www.jianshu.com/p/8a8f3743969b