也说扩展方法

    前一阶段看到园子里面有不少人写了很多关于web界面的扩展方法,由于工作原因,本人几乎不碰任何关于界面的东西,所以这里写的扩展方法都是纯粹的逻辑方面的扩展,并且集中在了对IEnumerable<T>方面的扩展。

    关于IEnumerable<T>方面的扩展,相信大家已经非常熟悉了,在Linq里面用的最多的就是IEnumerable<T>和IQueryable<T>的扩展方法。本文中主要还是对IEnumerable<T>添加一些扩展行为。

1.Zip方法

    熟悉函数式编程语言的话,一定知道Zip函数,其作用就是把两个序列(这里的序列就是指IEnumerable<T>的实例)合并成一个序列,并且这个结果序列的元素就是依次取这两个序列中的元素,通过某个函数合并而成的对象,结果序列的长度由短的那个序列决定。

    来看一下如何实现这样一个功能:

image

    是不是很意外,上边用这么多话说了zip要干什么,实现代码却只有4句(本文中的所有代码均不考虑参数检查),方法的声明都花了3行,这里不得不感谢c# 2.0就提供的yield语法糖,把原本复杂的实现全部交给编译器了。

    在看看这个zip方法用起来如何:

image

    看看运行结果:

image

    但是,现在还是仅仅处理了有限序列,如果是无限序列哪?

    首先,写个无限序列:

image

 

 

 

    在uncheck的情况下,这个序列是不可能有终止的。

    可能有人会惊呼while(true)这不是死循环嘛,程序会在这里卡死,不要紧张,这里while(true)里面是yield,所以不会有任何问题,除非你想把这个序列全部求解出来。

    再来看一下,values是一个从某数据源读取出来的一个有限序列,但是key序列并不知道应该初始化多少个,除非已经知道values的个数,但是在某些情况下,这个也是一个比较奢侈的要求,现在再用之前的zip方法:

image

    看一下结果:

image

    结果与之前一样,这因为zip在处理两个序列的时候,是根据较短的那个序列来决定结果序列的长度的,所有,当一个序列为有限,另一个为无限时,结果就为有限的那个序列的长度。

    但是,当两个无限都为无限序列的话哪?

    那么结果序列一定也是无限序列,这样对它的遍历就是死循环了,但是,两个无限序列做zip操作本身并不是无意义的。

    例如,现在需要一个无限序列,其第n个值等于(1+n)*(2+n),n>=0,好吧,当然可以利用前面的while(true)+yield方式再写一个,不过,本人比较懒惰,直接就用这个:

image

    来打印这个序列看看,不过要注意了,这个是无限序列,直接遍历就是死循环,所以,这里仅仅察看序列的前5个元素:

image

    其结果为:

image

    到这里,可以看出zip不但支持对有限序列的操作,同时也支持对无限序列的操作。

2.MergeJoin方法

 

 

 

 

    关系型数据库的Join有3种主要的Join方式:Nest Loop,Hash和Merge。其中的Merge方式就是这里的Merge Join的原理(wiki的解释)。

    同时,根据wiki上面的c#代码,可以很快写出这样一个实现(简易版):

image

    来调用一下看看:

image

    运行结果为:

image

    在这个两个数组中只有3和7是两个数组共有的,这个和直接Join的结果一样,看一下直接Join:

image

    非常简洁,说非常Linq,但是这个Join方式(Linq to Object)实际上用的是Hash方式,那么Hash Join方式和Merge Join方式到底有什么不同哪?

    Hash Join方式要求必须全部加载数据,而Merge Join方式只要求两个序列必须已经按照Key做排序。所以,如果数据量上G的话,Hash方式会导致内存不足,而直接失败(当然如果可以去改良),而Merge方式只要求序列排序,内存占用可以忽略不计,这样处理超大量的数据就不会有任何问题了,甚至可以是无限序列。

    例如:

image

    在不考虑溢出的情况下,a序列代表一个:(1+n)*(2+n),n>=0的一个无限序列,当然是一个递增的序列,而b序列代表一个10*n,n>=1的一个无限序列,当然也是一个递增的序列,c则是把a和b序列Join起来的一个序列,当然也是无限序列,然后打印出前5个元素:

image

    如果,这里想用Hash Join的方式,那么永远无法等到结果出来。

    最后来个复杂点的Join,作为Merge Join的功能展示(a,b都是按照Level递增的序列):

image 

3.LinearGroupBy方法

    要说到group by,相信无论是玩Linq的还是玩sql的都熟悉这个,先来看个标准的Linq:

image

    看看结果:

image

    不过,这里提出的LinearGroupBy的目的略有不同,group by是一个无视顺序的按键值分组,而LinearGroupBy是仅仅把相邻的元素group by起来,也就是说,{ 1, 1, 2, 2, 2, 3, 2 }经过LinearGroupBy的结果应该是:

image

    好吧,再来一个简易版的实现:

image

    调用代码则为:

image

    那么这个LinearGroupBy是实现了,但是这个方法与标准的group by相比,除了逻辑不一样,还有什么不同哪?

    这个LinearGroupBy的逻辑只需要知道前一个键值和当前元素,所以不需要知道序列的完整内容,换而言之,也就是可以处理无限序列。

 

 

 

 

 

 

 

 

 

    不过,如果这个序列本身就已经按照键值排序了哪?

    那么,标准group by与LinearGroupBy的结果将是一样的,也就是说,对于一个已经排序的序列,LinearGroupBy拥有更好的性能和更好的无限序列的亲和性:

image

    x是一个这样的序列:{1,2,2,3,3,3,5,6,6,7,7,7,9,…}(换句话说:0个0,1个1,2个2,3个3,0个4,1个5,2个6,3个7,0个8,1个9,…),本身就是一个已经排序的无限序列,看一下前五个group by的结果:

image

 

 

 

4.总结

    前面的3个方法,可以说都是为了无限序列和小内存操作而优化的,所以在需要用流的方式处理某些数据时非常有用。至于例子就暂时不写了,就说说领域吧,超大文件的处理,或者超大数据库表的用IDataReader的方式来处理,或者基于时间的无限序列的处理等方面。

你可能感兴趣的:(方法)