导语
一般常用的高阶函数函数有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的特性来实现惰性调用的。这些高阶函数求出来的值,在需要的时候才会真正的执行循环体的调用。有很多细节需要理解,需要注意,后续我会详细的举例子来说明这些细节。