概述
文章目录
- 起因
- 为什么叫"管道"
- 从循环说起
- 一. 典型问题
- 二. 循环迭代处理
- 三. 管道处理
- 1. 手写管道
- 2. 提取操作
- 3. 使用泛型
- 4. 使用LINQ
- 5. 其他语言
- 语言之外的扩展
起因
近来在看《重构(第二版)》,里面有提到一个重构模式是“以管道取代循环(Replace Loop with Pipeline)”,这让我想起来去年年初的时候学习C++在powershell上的遇到的“管道”,当时还专门写了一篇博客记录了一下:Windows PowerShell的“管道”以及对可执行文件的文件重定向,加上最近又在学习shader,跟渲染管线打交道比较频繁,突然感觉这些东西的思想都是一致的——
把一批操作组合成一个操作序列,然后再把需要处理的一个或一批对象扔到这个操作序列里,在序列的最后获取操作结果。,
接下来又联想到C#里的LINQ常用操作,以及很久以前使用过的DOTween插件,无一不是使用了这种管道的编程思想,于是决定把这种编程思想记录一下。
为什么叫"管道"
为什么叫“管道”?想想下面这个图,假设每一个阀门都是一个处理点,那么我们把材料从入口放入,那么所有的材料都会一一通过每一个处理点,每一个处理点进行的操作各不相同,但我们要做的只是在出口等待,管道最后会把最后结果给我们。
从循环说起
从哪里开始是一个不大不小的问题,想了想,还是从集合开始吧。
“管道”的编程思想,在重构作者的个人网站里有一篇文章有专门论述,他把这种编程方式叫做“集合管道模式(Collection Pipeline Pattern)”,英文好的同学也可以直接查看:Collection Pipeline,之所以会有“集合”这个前缀,跟管道的特点也是离不开的。
我们还是从代码开始。
一. 典型问题
先考虑一个经典的场景:
期末考试学生们进行了考试,每个学生都需要考语数英三门课程,每门课程满分100。
我现在需要统计一下这些学生三门课程平均分超过了60分的人的名单。
以下代码使用C#编写。
按常规思路,我们先对学生建模,创建一个学生类:
public class Student
{
public string name;
public int maths;
public int chinese;
public int english;
public int Average
{
get
{
return (maths + chinese + english) / 3;
}
}
}
二. 循环迭代处理
好了,理一理思路,我们的思路可能会是这样:
- 我们创建一个
List
用于保存满足条件的学生的名字- 再对所有学生进行一次遍历
- 逐个判断单个学生是否满足了要求,如果满足要求,就把学生的名字加入到满足条件的列表中
- 遍历完成,获得结果
那么我们最常见的使用循环迭代的处理方式会是如下:
public static List<string> GetPass60(Student[] students)
{
List<string> result = new List<string>();
foreach (var stu in students)
{
if(stu.Average >= 60)
{
result.Add(stu.name);
}
}
return result;
}
这样写当然没有问题,我们也获得了我们想要地结果,但是还是显得有些累赘。
如果我们要再取学生平均成绩在90分以上的呢?要取三门成绩相差不超过10分的呢?要取前十名呢?每一次都重新写几个循环吗?无论是复用性也好,还是可读性也好,光是想想我都觉得头大,感觉一坨屎山已经从天而降。
三. 管道处理
如果是用管道的思想又会是怎么样呢?
- 选出所有平均分达到60分的学生
- 把这些学生的名字加入结果列表中
这时候肯定有同学举手了,“你这明明是耍赖,如果能一次选出来结果,我当然一次选出来了!”
但是要注意,这正是管道编程思想和迭代思想的不同之处,对管道编程的思想来说,所有的学生是同时被“扔”进管道进行处理的,我们要做的其实是两步:先筛选,再把名字加入列表。在这里,“所有的学生”是一个整体,也就是所谓的“集合(Collection)”——而这正是“集合管道(Collection)”的由来。
在这种思想里,所有的学生天然地会被逐个处理,但是这个过程是不在我们的计算过程内的。
talk is cheap, show me the code.
继续进入代码世界。
1. 手写管道
前面有说过,管道天然会逐个处理对象,那么我们首先要做好基础设施建设,针对这次的需求来两根管道。
//筛选符合条件的学生
public static List<Student> FilterStudentPipe(this List<Student> students)
{
List<Student> result = new List<Student>();
foreach (var stu in students)
{
if (stu.Average >= 60)
{
result.Add(stu);
}
}
return result;
}
//将所有符合条件的学生的名字记录下来
public static List<string> SelectStudentNamePipe(this List<Student> students)
{
List<string> result = new List<string>();
foreach (var stu in students)
{
result.Add(stu.name);
}
return result;
}
管道搭好,接下来我们就可以进行操作了:
public static List<string> GetPassStudents(List<Student> students)
{
//连接两次操作
return students.SelectStudentNamePipe().FilterStudentPipe();
}
2. 提取操作
这时候肯定又双叒叕有同学要说了,你这还多出来好多行代码,甚至变得又长又臭——但是考虑另一个问题,现在的要求是提取所有平均分60分及以上的同学,但如果要求提取90分以上的呢?
所以下一步我们需要把操作提取出来。
public static class ExcuteClass
{
//增加pass操作,使用外部传入的函数进行判断
public static List<Student> FilterStudentPipe(this List<Student> collection,Func<Student, bool> pass)
{
List<Student> result = new List<Student>();
foreach (var stu in collection)
{
if (pass(stu))//使用pass进行判断
{
result.Add(stu);
}
}
return result;
}
public static List<string> SelectStudentNamePipe(this List<Student> students, Func<Student, string> selector)//增加selector操作,使用外部传入的函数进行转换
{
List<string> result = new List<string>();
foreach (var stu in students)
{
result.Add(selector(stu));//使用selector转换
}
return result;
}
//连接两个函数,把操作放入
public static List<string> GetPassStudents(List<Student> students)
{
return students.FilterStudentPipe(stu=> stu.Average > 60)
.SelectStudentNamePipe(stu=>stu.name);
}
}
3. 使用泛型
到了这一步就足够了吗?
仔细想一想,再进行一层抽象:
我们第一步在本质上其实是传入了一批数据,然后再传入了一个判断是否满足条件的函数,最后返回了所有满足条件的数据
而我们的第二步,本质上其实是传入了一批数据,然后再传入了一个用于转换的函数,最后返回了所有转换完成的数据
注意:这个过程是与数据本身是什么类型是无关的!
也就是说,我们现在可以用它来筛选学生,下一次我们可以用同样的步骤来筛选老师!
这一次我们可以从学生身上获取名字,下一次我们可以用同样的步骤来获取学生的分数!
所以我们就有了泛型的函数:
public static class ExcuteClass
{
public static List<T> Filter<T>(this List<T> collection,Func<T, bool> pass)
{
List<T> result = new List<T>();
foreach (var item in collection)
{
if (pass(item))
{
result.Add(item);
}
}
return result;
}
public static List<TResult> Select<TInput,TResult>(this List<TInput> collection, Func<TInput, TResult> selector)
{
List<TResult> result = new List<TResult>();
foreach (var item in collection)
{
result.Add(selector(item));
}
return result;
}
//注意这个函数
public static List<string> GetPassStudents(List<Student> students)
{
return students.Filter(stu=> stu.Average > 60)
.Select(stu=>stu.name);
}
//获取各种奇奇怪怪的结果
public static void GetOther(List<Student> students)
{
//获取所有名字字数在两个以上的学生的名字
List<string> name = students.Filter(stu => stu.name.Length > 2)
.Select(stu=>stu.name);
//获取所有数学和语文都及格了的学生的英文的成绩
List<int> engScores = students.Filter(stu => stu.maths > 60)
.Filter(stu => stu.chinese > 60)
.Select(stu=>stu.english);
}
}
第三步我们把筛选和转换的过程完全抽象了出来,注意我们最后的调用函数,完全没有任何变化!
而与此同时,我们可以用它们来相当简单地获取各种各样奇奇怪怪的数据,不仅如此,我们还获得了易读性要强得多的代码!
4. 使用LINQ
如果你还觉得每次都要写几个抽象的函数很累,那么如果你使用的是C#,可以直接使用LINQ,LINQ里已经实现了非常多的对集合进行处理的函数——包括我前面实现的Filter
和Select
。
//我们前面实现的Filter
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
//我们前面实现的Select
public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector);
如果使用LINQ,我们前面的代码只需要改成这样:
public static List<string> GetPassStudents(List<Student> students)
{
return students.Where(stu => stu.Average > 60)
.Select(stu => stu.name)
.ToList();
}
public static void GetOther(List<Student> students)
{
//获取所有名字字数在两个以上的学生的名字
List<string> name = students.Where(stu => stu.name.Length > 2)
.Select(stu=>stu.name)
.ToList();
//获取所有数学和语文都及格了的学生的英文的成绩
List<int> engScores = students.Where(stu => stu.maths > 60)
.Where(stu => stu.chinese > 60)
.Select(stu=>stu.english)
.ToList();
}
对比一下一开始的代码,是不是简洁漂亮了太多?
5. 其他语言
前面这么多例子,都是C#的,但是支持这种思想的远远不只C#,比如javascript
同样提供了几个常用的管道函数,例如map
、filter
等等,不仅如此,javascript
甚至提供了专门的管道操作符,可以将前一个函数的返回值直接传给后一个。
let arr = [1,2,3,4,5,6,7,8,9]
let ascii = arr.map(v=> v * 2)
.filter(v=>v > 5)
.map(v => v * 2 + 52)
.map(v => String.fromCharCode(v));
console.log(ascii); //输出 [ '@', 'D', 'H', 'L', 'P', 'T', 'X' ]
const double = (n) => n * 2;
const increment = (n) => n + 1;
// 没有用管道操作符
double(increment(double(5))); // 22
// 用上管道操作符之后
5 |> double |> increment |> double; // 22
语言之外的扩展
前面我实现了一个自己的“管道”,但是管道的思想远不止如此,我们可以回到文章的开头:把一批操作组合成一个操作序列,然后再把需要处理的一个或一批对象扔到这个操作序列里,在序列的最后获取操作结果。
这个思想可以在很多地方应用,并且已经被非常多的地方应用了:
-
游戏引擎的渲染管线:
-
PowerShell的管道
指令A | 指令B | 指令C (enter)
- CPU指令流水线
参考:
- https://blog.csdn.net/u014106644/article/details/95209474
- https://martinfowler.com/articles/collection-pipeline/
最后
以上就是狂野导师为你收集整理的对“管道”的进一步理解的全部内容,希望文章能够帮你解决对“管道”的进一步理解所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复