工控智汇

工控智汇

用 LINQ 编写 C 都有哪些一招必杀的技巧?

admin 43 100

前言

限于篇幅,本文的内容是启发性的,目的是让你知道LINQ的原理和知道原来LINQ还可以这么用。至于一些细节,则还需要你额外地学习。

俗话说,纸上得来终觉浅,为了避免纸上谈兵,本文不可避免地会列出很多代码,理解这些代码的最好方式是自己动手尝试下。然而,也许你的电脑上还暂时没有开发环境。

虽然安装开发环境不是难事,但是总归会成为一些人动手尝试的障碍。为了方便你立刻动手进行探索,首先推荐一个网站给读者,这个网站叫做,顾名思义,它提供了一个在线的编辑、编译调试环境,你只要贴上代码,并且提供输入的行,它就能在线编译执行并且返回结果。

这个网站支持很多编程语言,C编译器略有不同,一些新的语法可能不支持。

另外,这个网站也并不能在线运行Winforms或者程序,所以它并不能完全代替你在自己的PC上练习和开发程序。

在浏览器里输入,打开网站,可以看到,默认下编辑语言是Java,我们点Java下拉箭头,出现一个语言选择的菜单,我们可以选择C代码没有什么区别。

LINQ有两种写法,LINQ表达式写法和调用LINQ操作符(查询方法)。比如如下两个简单的查询,实现了相同的功能,将一个数组中的偶数挑出来:

使用LINQ表达式:

int[]arr={1,2,3,4,5,6,7,8};varquery=fromxinarrwherex%2==0selectx;foreach(intxinquery)(x+"\t");

使用LINQ操作符:

int[]arr={1,2,3,4,5,6,7,8};varquery=(x=x%2==0);foreach(intxinquery)(x+"\t");

如果你使用ideone来编写如上的代码,请记得在开头加上:

;

以上两段代码是等价的。初看这两段代码,让人觉得很困惑。原因是,第一段代码看上去像SQL查询,但是写法却和SQL略有不同,给人的印象是,似乎C编译器会自动将LINQ表达式转换为对LINQ操作符的调用。所以你的代码实际上相当于:

varquery=(x=x%2==0);

而在这段代码里,我们其实调用了Where方法。现在你可能会想,如果我们自己写一个Where方法,结果会如何呢?我们来试试看:

usingSystem;;namespaceConsoleApp1{staticclassMyLinq{publicstaticIEnumerableintWhere(thisint[]arr,Funcint,boolcond){returnnewint[]{2,4,6,8};}}classProgram{staticvoidMain(string[]args){int[]arr={1,2,3,4,5,6,7,8};varquery=fromxinarrwherex%2==0selectx;foreach(intxinquery)(x+"\t");}}}

或者参见:

尝试运行下,结果是:

2468

细心的读者看到int[]arr前面有个this,这说明这是一个扩展方法,扩展方法允许将静态方法模拟成第一个参数所代表的对象的成员方法。关于扩展方法的有关内容不是本文的讨论范围。

有需要了解的可以参考这篇文章:()

看,我们实现了一个最基本的LINQ。这代码看上去太简单了吧,看着让人怀疑啊,你肯定想试试看,把arr修改为{1,2,3,4,5,6,7,8,9,10}。一试,果然露馅了。结果还是2468,没有10。

这很好理解,因为我们是硬编码返回的2,4,6,8,并没有将数组传入,也没有将条件传入,自然结果是写死的。这样的“LINQ”自然没用。另外Funcint,boolcond这是什么鬼,看着又奇怪了。

别着急,这个叫做委托。所谓委托,就是用它表示一个函数,这个函数从调用者看,可以是任意的函数名,甚至没有函数名,从被调用者看,它叫做cond,我们直接调用它就可以了。后面我们再详细说,看下面的代码:

publicstaticIEnumerableintWhere(thisint[]arr,Funcint,boolcond){int[]result=newint[];intj=0;for(inti=0;;i++)if(cond(arr[i]))result[j++]=arr[i];(refresult,j);returnresult;}

或者参见:

这次运行正确了,输出246810。

不信?你可以修改下条件,比如输出比5小的:

varquery=fromxinarrwherex5selectx;

怎么样,可以吧。为什么这段代码可以过滤任意的条件呢?奥妙就在cond这个委托上,对于内部来说,我们看上去像有这么一个函数:

boolcond(intx){?}

然而实际上我们的程序里没有这么一个函数,而这个函数实际上就是查询里面的where?

?代表这个函数的实现。

之前我们说了,LINQ还可以用LINQ操作符来表示,比如varquery=fromxinarrwherex%2==0selectx;可以写成:

varquery=(x=x%2==0);

完整的代码是:

委托相当于一个方法,我们既可以传Lambda表达式,也可以传一个传统的方法,比如返回小于5的数字,我们可以定义:

staticboolmyfunc(intx){returnx5;}

于是我们可以写:

varquery=(myfunc);

注意这代码等价于(x=x5);

完整的代码:

因此,我们知道了,x=x5的x是怎么回事,其实它相当于你定义了一个函数(和myfunc类似),而x是这个函数的参数,这个函数被传入Where,由Where调用,每次遍历一个元素就会调用一次,每次x代表数组中的一个元素,判断你的条件并且返回是否应该被放入结果还是应该舍弃。

因此,x就像你写的函数的参数一样,它可以任意命名x=x5和y=y5是一样的。好比:

staticboolmyfunc(inty){returny5;}

将这个函数的参数全部修改为y,这个函数和之前定义的并没有什么区别。

让我们对LINQ的本质做一个简单的总结。LINQ表达式会被C代码,而没有幕后任何玄妙的机制。Lambda表达式的本质是一个匿名的方法,箭头前面的部分是它的参数,后面的语句就是这个函数的返回值。

几个常见的LINQ操作符和它们的使用技巧

限于篇幅,这里只能以点带面地介绍几个最频繁使用的LINQ操作符,掌握它们你就可以写出很多有趣的程序,而且对它们原理的揭示,将会有助于你自学其它的LINQ操作符。

第一个要提到的是select,它的作用是投影,对一个序列的每一项做一个运算,得到一个结果,而select的结果是一个和原序列等长的新的序列,它的每一项是经过运算变化以后的每一个结果。

比如对于arr={1,2,3,4}来说,arr2=(x=x*2),则arr2={2,4,6,8}。对于ListPersonlist来说,names=(x=),结果是原来Person集合中每个元素的name字段。

Select有个很有用的重载形式,是SelectTSource,TResult(IEnumerableTSource,FuncTSource,Int32,TResult)的形式,注意其中的委托的Int32参数,写起来一般是Select((x,i)=?)这样的形式,这个i代表了此元素在序列中的位置。

下面的代码演示了这个重载的用法:

strings="hello";varquery=((x,i)=new{x,i});foreach(variteminquery)(item);

结果是:

{x=h,i=0}{x=e,i=1}{x=l,i=2}{x=l,i=3}{x=o,i=4}

可以看出,i是以0开始的元素的下标。关于Select操作符更多信息,具体可以参考:

第二个要提到的是GroupBy,它的作用是分组,因此原始序列按照分组规则能分多少组,那么结果序列的长度就是几。而结果序列的每一项,又是一个序列,这个序列是所有符合这个分组规则的原始数据的每一项。

对于结果序列来说,还有一个Key属性,代表分组的规则。

比如对学生成绩以10分为单位分组,代码如下:

int[]scores={90,65,82,71,84,88,52,78,61,75,85,79};varquery=(x=x).GroupBy(x=x/10);foreach(varginquery){("groupkey={0}values=",);foreach(variteming)(item+"");();}

运行结果:

groupkey=5values=52groupkey=6values=6165groupkey=7values=71757879groupkey=8values=82848588groupkey=9values=90

下面的图可以很好地展示一个分组的过程。

Scores9065827758579

分组后:

QueryKey=552Key=66165Key=771757879Key=882848588Key=990

第三个要提到的是SelectMany,它的作用是对于一个序列的序列,将一个序列的每一项提取出来作为结果的每一项。因此它有点类似GroupBy的反操作:

如下代码的query2将会把query的所有的分组又放入一个序列中。

int[]scores={90,65,82,71,84,88,52,78,61,75,85,79};varquery=(x=x).GroupBy(x=x/10);varquery2=(x=x);foreach(variteminquery2){(item+"");}

结果是:

5261657858890

SelectMany最常用的操作是生成笛卡尔集,也就是把第一个集合的每一项和第二个集合的每一项匹配,得到数量为两个集合元素数量相乘的新的集合。

比如:

string[]fruits={"橘子","香蕉","西瓜","苹果"};string[]people={"张三","李四","王二麻"};varquery=(x=(y=x+"喜欢吃"+y));foreach(variteminquery){(item);}

结果是:

张三喜欢吃橘子张三喜欢吃香蕉张三喜欢吃西瓜张三喜欢吃苹果李四喜欢吃橘子李四喜欢吃香蕉李四喜欢吃西瓜李四喜欢吃苹果王二麻喜欢吃橘子王二麻喜欢吃香蕉王二麻喜欢吃西瓜王二麻喜欢吃苹果

对于一个4个元素和一个3个元素的集合的笛卡尔积,是12个元素,如上。

以上代码的SelectMany还可以通过它的另一种重载形式简化:

varquery=(x=fruits,(x,y)=x+"喜欢吃"+y);

这段代码和之前的代码,作用是一样的。特别需要指出的是,在不同的Lambda表达式中,相同的变量名其实没有任何关系,比如第一个x=fruits和第二个(x,y)=x+"喜欢吃"+y的x就是两回事。

这也很好理解,比如有两个不同的函数,它们都有参数x,显然它们没有任何联系。

Skip和Take操作符。顾名思义,Skip(n)在指定的序列上跳过n个元素,而Take(n)则取n个元素。如果Skip和Take的n比序列上剩下的元素多,那么执行不会报错,但是会返回能返回的最多的元素。

比如arr={1,2,3,4,5},(1)返回{2,3,4,5}而(1).Take(2)返回{2,3},(1).Take(10),因为序列中并没有那么多元素,所以返回{2,3,4,5},(100).Take(10)则返回空序列。

Skip和Take最常见的用法是做分页。

最后我想介绍下Aggregate,聚合函数,它对于查询的时候需要前面元素参与计算的需求非常有用。比如下面的代码,可以根据前后两个元素得到一个结果:

string[]stations={"北京","石家庄","郑州","武汉","衡阳","广州"};Liststringresult=newListstring();varquery=(1).Aggregate(stations[0],(acc,curr)={(acc+"-"+curr);returncurr;},x="");foreach(variteminresult){(item);}

结果如下:

北京-石家庄石家庄-郑州郑州-武汉武汉-衡阳衡阳-广州

LINQ中还有一些操作符,比如OrderBy,Join,Distinct,和它们在数据库查询中的用法类似,这里限于篇幅就不再展开介绍了。

初学者容易犯的错误

LINQ中大部分情况下返回的是序列,序列没有办法直接赋值给数组、列表,并且,LINQ是延迟查询的,只有用foreach迭代,它才会真正执行。用ToArray和ToList可以立刻执行并且放入数组、列表。看下面的代码:

Listintlist=newListint(){1,2,3,4,5,6,7,8};varquery=(x=x%2==0);(10);(12);foreach(intiinquery)(i+"");

结果是24681012,因为LINQ是延迟查询的。为了固定查询结果,我们可以加上ToList:

varquery=(x=x%2==0).ToList();

这样就不会出现10和12了。如果我们希望删除list里的奇数项,也许你会这么写:

Listintlist=newListint(){1,2,3,4,5,6,7,8};list=(x=x%2==0);

然而这样写是不能通过编译的,这是因为list是序列(IEnumerable类型),而不是List。为此,我们也可以加上ToList:

Listintlist=newListint(){1,2,3,4,5,6,7,8};list=(x=x%2==0).ToList();

这样写就没问题了。请注意,像Select/Where/Take/GroupBy/OrderBy/Join这样的查询,返回的是序列,即便序列是空的,或者只有一个元素。比如:

Useru=(x===1);

这是不行的,虽然这样查询,返回的序列只有一个元素,但是一个元素的序列还是序列,而不是这个元素本身。类似一个装着一个苹果的篮子,是篮子,而不是苹果。

正确的做法是使用Single或者First操作符来得到这个序列的唯一元素或者第一个元素。

Useru=(x===1).First();

这样就可以了。类似地,如果用Take(1)取得一个元素,也需要用First():

Useru=(1).First();

以上代码等价为:

Useru=();

也有一些操作符,比如Single/First,返回的是单个的元素,除此之外,Max()、Average()、Aggregate()之类的聚合方法,也是返回的单一元素。

LINQ在C制作一个年历

这是一个经典的面试题,用LINQ可以简化代码的编写。

代码如下:

usingSystem;;;;namespaceConsoleApplication1{classProgram{staticvoidMain(string[]args){stringcalar="";calar=((1,12)groupxby(x+2)/3intogselect(BuildCalar(,()[0]).Split(newchar[]{'\r','\n'},).Union(newstring[]{"\r\n"}).Zip(BuildCalar(,()[1]).Split(newchar[]{'\r','\n'},).Union(newstring[]{"\r\n"}),(x,y)=().PadRight(23,'')+y).Zip(BuildCalar(,()[2]).Split(newchar[]{'\r','\n'},).Union(newstring[]{"\r\n"}),(x,y)=().PadRight(46,'')+y)).Zip(("\r\n",8),(x,y)=x+y).Aggregate((serials,current)=serials+current)).Aggregate((serials,current)=serials+current);(+"\r\n"+calar);}staticstringBuildCalar(intyear,intmonth){stringcalar=newstring[]{(),"SUMOTUWETHFRSA"}.Union((1-(int)newDateTime(year,month,1).DayOfWeek,newDateTime(year,month,1).AddMonths(1).AddDays(-1).Day+(int)newDateTime(year,month,1).DayOfWeek).GroupBy(x=((x+(int)(newDateTime(year,month,1).DayOfWeek+6))/7),(key,g)=new{GroupKey=key,Items=g}).Select(x=(y=y1?"":(y).PadLeft(2,'0')+"").Aggregate((serials,current)=serials+current))).Aggregate((serial,current)=serial+"\r\n"+current);returncalar;}}}

这是运行结果:

你也可以通过ideone得到在线的程序,。如果你看到这个程序的时候是2018年了,那么fork一份重新运行,会得到2018年的年历。

第四个例子:使用LINQ代码简化递归遍历

对于数组或者集合,我们直接使用foreach调用就好了,但是如果我们要遍历层次结构怎么办呢?必须定义一个方法,递归调用。

虽然遍历的代码从结构上看大同小异,但是具体到不同的场景,比如遍历数据库中的字段、遍历控件、遍历文件系统、遍历二叉树、遍历TreeView……,则需要编写不同的代码,似乎不太好进行代码的重用。

下面给出的例子,就是Lambda表达式大显身手的地方了。我们可以用它写出一个通用的代码,借助它,再遍历各种层次结构,都可以轻松搞定。

我们使用VS新建一个WinForms程序,在主窗体上添加一个菜单条(MenuStrip)、一个TreeView、三个按钮(用于遍历菜单、TreeView和控件)、一个ListBox(用于输出结果)、以及若干控件构成的层次结构。

创建菜单条后,我们可以利用“插入标准项”功能快速插入一些标准的菜单条目,如图所示:

为了演示递归,我们给菜单条多加上一些层次和项目:

完成的界面如下:

添加一个类,比如叫class1,然后编写如下代码:

usingSystem;;;;;namespaceWindowsFormsApp1{staticclassClass1{privatestaticIEnumerableTNodeGetChildrenTNode(TNodenode,FuncTNode,IEnumerableTNodeGetNodes){varnodes=GetNodes(node);((x=GetChildren(x,GetNodes)));}publicstaticIEnumerableTNodeGetChildrenRecursivelyTRoot,TNode(thisTRootobj,FuncTRoot,IEnumerableTNodeEnumRoot,FuncTNode,IEnumerableTNodeGetNodes){varnodes=EnumRoot(obj);((x=GetChildren(x,GetNodes)));}}}

然后我们就可以使用了。首先双击第一个按钮,我们来遍历菜单条:

privatevoidbutton1_Click(objectser,EventArgse){varitems=(x=(),x=());();foreach(variteminitems)();}

运行结果:

然后双击第二个按钮,遍历TreeView:

privatevoidbutton2_Click(objectser,EventArgse){varitems=(x=(),x=());();foreach(variteminitems)();}

运行结果:

最后,第三个按钮,遍历控件:

privatevoidbutton3_Click(objectser,EventArgse){varitems=(x=(),x=());();foreach(variteminitems)();}

运行结果:

如果我们在结尾处加上OfTypeTextBox,那么我们可以遍历界面上所有的文本框。

如果我们需要对界面上所有的输入做一个统一判断,避免任意一个文本框为空,那么这段代码也可以用来做控件的验证。

此递归代码不但可以用来遍历各种树状结构,甚至也可以用来解决之前说的排列组合问题:

int[]arr={1,2,3,4};varquery=,IEnumerableint(x=(y=newint[]{y}),x=(x).Select(y=(newint[]{y}.ToArray())));foreach(variteminquery){foreach(variinitem)(i+"");();}

完整的程序:

运行结果:

12341223412433424322123242432312342332443123234224321

可以看到,Lambda表达式允许我们在一个方法中将需要重用的算法主体先写出来,然后将需要自定义的地方用Lambda表达式交给调用者实现,从而让编写的类库代码具有更大的重用价值。

第五个例子:读取和写入文件

在.以后的版本中,命名空间的File静态类下,多了几个很实用的文件读写方法:

使用它们可以很方便地读写文件。假设我们有一个文本文件,叫做1.txt,里面包含以下内容:

12345678910

我们希望编写一个程序求和,我们可以这么写:

varlines=(@"X:\path\1.txt");varsum=(x=(x)).Sum();(sum);

结果是55。

用表达式树构建查询

在前面的介绍中,我们的LINQ查询都是在代码中写好的,然而有时候我们希望在代码运行的过程中动态产生一个条件判断的Lambda表达式,这就需要使用表达式树来构建。

让我们回到文章开始的那个例子,把数组中的偶数挑选出来。但是我们这次使用表达式树来动态生成和编译Lambda表达式。

为了使用表达式树,我们需要先导入如下命名空间:

;

先看代码:

int[]arr={1,2,3,4,5,6,7,8};varparam=(typeof(int),"x");varmodexp=(param,(2));varbody=(modexp,(0));ExpressionFuncint,boolcond=,bool(body,param);varquery=(());foreach(intxinquery)(x+"\t");

完整代码参考:

这段代码创建了一个叫做cond的表达式树,它实现了类似x=x%2==0的Lambda,我们在运行期间构造了它并且用Compile编译成方法,传入where执行了查询。

cond的结构如下:

我用括号标注了对应的表达式等价的代码。可以看到condbodymodexp三个节点分别代表Lambda表达式的=、==和%三个二元操作符。0和2代表常数节点,而param则是Lambda表达式的参数。

使用表达式树还可以调用函数甚至包括循环、判断等代码逻辑。限于篇幅,这里不展开介绍了。

总结

本文首先介绍了LINQ的本质,它是利用类库中编写好的一组代码实现的,完全在内存中,由C#代码执行的数据操作。特别需要理解的是,Lambda表达式的用法。

然后介绍了几个LINQ的操作符,所有的操作符都可以在命名空间中找到,并且一些操作符有不止一个重载形式。

接下来,我们给出了几个LINQ的使用例子,尽管代码对于初学LINQ的读者有些偏难,但是借助MSDN和Google,读者可以体会到LINQ编程的简洁和便利。

最后,我们介绍了表达式树的概念,这种动态创建代码的方式也被称作元编程(metaprogramming)。希望这篇文章能够给你一些有趣的信息,并且让你对LINQ有一个初步的了解。

限于篇幅,很多内容并没有深入,不过不要紧,在文章最后,我提供了一些参考书籍和链接,让你可以进一步学习LINQ这项有用的技术。