全面分析c# LINQ
大家好,这是[C#.NET拾遗补漏]系列的第08篇文章,今天讲C#强大的LINQ查询。LINQ是我最喜欢的C#语言特性之一。
LINQ是LanguageINtegratedQuery单词的首字母缩写,翻译过来是语言集成查询。它为查询跨各种数据源和格式的数据提供了一致的模型,所以叫集成查询。由于这种查询并没有制造新的语言而只是在现有的语言基础上来实现,所以叫语言集成查询。
一些基础
在C#中,从功能上LINQ可分为两类:LINQtoObject和LINQtoXML;从语法上LINQ可以分为LINQtoObject和LINQ扩展方法。大多数LINQtoObject都可以用LINQ扩展方法实现等同的效果,而且平时开发中用的最多的是LINQ扩展方法。
LINQtoObject多用于映射数据库的查询,LINQtoXML用于查询XML元素数据。使用LINQ查询的前提是对象必须是一个IEnumerable集合(注意,为了描述方便,本文说的集合都是指IEnumerable对象,包含字面上的ICollection对象)。另外,LINQ查询大多是都是链式查询,即操作的数据源是IEnumerable
形如下面这样的查询就是LINQtoObject:
varlist=fromuserinusers whereuser.Name.Contains("Wang") selectuser.Id;
等同于使用下面的LINQ扩展方法:
varlist=users .Where(u=>user.Name.Contains("Wang")) .Select(u=>u.id);
LINQ查询支持在语句中间根据需要定义变量,比如取出数组中平方值大于平均值的数字:
int[]numbers={0,1,2,3,4,5,6,7,8,9}; varresult=fromnumberinnumbers letaverage=numbers.Average() letsquared=Math.Pow(number,2) wheresquared>average selectnumber; //平均值为4.5,result为{3,4,5,6,7,8,9}
其中的Select方法接收的参数用的最多的是Func
varcollectionWithRowNumber=collection. .Select((item,index)=>new{Item=item,RowNumber=index}) .ToList();
再来看一下LINQtoXML的示例。假如我们有如下XML文件:
1 Liam 男 2 ...
使用LINQtoXML查询所有含有指定节点值的元素:
XElementxelement=XElement.Load("Employees.xml"); varels=fromelinxelement.Elements("Employee") where(string)el.Element("Sex")=="Male" selectel;
等同于使用LINQ扩展方法:
varels=xelement.Elements("Employee") .Where(el=>(string)el.Element("Sex")=="Male");
LINQtoXML操作XML非常方便和灵活,大家可以在具体使用的时候去探索,这里就不展开讲了。
LINQ查询有很多方法,由于篇幅原因,就不一一列举演示了,这里只选取一些强大的查询方法,这些方法若使用非LINQ来实现可能会比较麻烦。
LINQ之所以强大,是因为它可以轻松实现复杂的查询,下面我们来总结一下C#LINQ的强大之处。
Fist、Last和Single等
First、FirstOrDefault、Last、LastOrDefault、Single和SingleOrDefault是快速查询集合中的第一个或最后一个元素的方法。如果集合是空的,Fist、Last和Single都会报错,如果使其不报错而在空集合时使用默认值可以使用FirstOrDefault、LastOrDefault和SingleOrDefault。Single/SingleOrDefault和其它方法的区别是,它限定查询结果只有一个元素,如果查询结果集合中包含多个元素时会报错。具体看下面几个示例:
new[]{"a","b"}.First(x=>x.Equals("b"));//返回”b“ new[]{"a","b"}.First(x=>x.Equals("c"));//抛出InvalidOperationException异常 new[]{"a","b"}.FirstOrDefault(x=>x.Equals("c"));//返回null new[]{"a","b"}.Single(x=>x.Equals("b"));//返回”b“ new[]{"a","b"}.Single(x=>x.Equals("c"));//抛出InvalidOperationException异常 new[]{"a","b"}.SingleOrDefault(x=>x.Equals("c"));//返回null new[]{"a","a"}.Single();//抛出InvalidOperationException异常
在实际应用中,如果要确保查询结果的唯一性(比如通过手机号查询用户),使用Single/SingleOrDefaut,其它情况应尽量使用First/FirstOrDefault。虽然FirstOrDefault也可以根据条件判断元素是否存在,但使用Any更高效。
Except取差集
LINQ的Except方法用来取差集,即取出集合中与另一个集合所有元素不同的元素。
示例:
int[]first={1,2,3,4}; int[]second={0,2,3,5}; IEnumerableresult=first.Except(second); //result={1,4}
注意Except方法会去除重复元素:
int[]second={0,2,3,5}; int[]third={1,1,1,2,3,4}; IEnumerableresult=third.Except(second); //result={1,4}
对于简单类型(int、float、string等)使用Except很简单,但对于自定义类型(或者叫复合类型,下同)的Object如何使用Except呢?此时需要将自定义类型实现IEquatable
classUser:IEquatable{ publicstringName{get;set;} publicboolEquals(Userother) { returnName==other.Name; } publicoverrideintGetHashCode() { returnName?.GetHashCode()??0; } } classProgram { staticvoidMain(string[]args) { varlist1=newList { newUser{Name="User1"}, newUser{Name="User2"}, }; varlist2=newList { newUser{Name="User2"}, newUser{Name="User3"}, }; varresult=list1.Except(list2); result.ForEach(u=>Console.WriteLine(u.Name)); //输出:User1 } }
SelectMany集合降维
SelectMany可以把多维集合降维,比如把二维的集合平铺成一个一维的集合。举例:
varcollection=newint[][] { newint[]{1,2,3}, newint[]{4,5,6}, }; varresult=collection.SelectMany(x=>x); //result=[1,2,3,4,5,6]
再来举个更贴合实际应用的例子。例如有如下实体类(一个部门有多个员工):
classDepartment { publicEmployee[]Employees{get;set;} } classEmployee { publicstringName{get;set;} }
此时,我们拥有一个这样的数据集合:
vardepartments=new[] { newDepartment() { Employees=new[] { newEmployee{Name="Bob"}, newEmployee{Name="Jack"} } }, newDepartment() { Employees=new[] { newEmployee{Name="Jim"}, newEmployee{Name="John"} } } };
现在我们可以使用SelectMany把各部门的员工查询到一个结果集中:
varallEmployees=departments.SelectMany(x=>x.Employees); foreach(varempinallEmployees) { Console.WriteLine(emp.Name); } //依次输出:BobJackJimJohn
SelectMany迪卡尔积运算
SelectMany不光适用于单个包含多维集合对象的降维,也适用于多个集合之前的两两相互操作,比如进行迪卡尔积运算。比如我们有这样两个集合:
varlist1=newList{"a1","a2"}; varlist2=newList {"b1","b2","b3"};
现在我们需要把它进行两两组合,使用普通的方法,我们需要用嵌套循环语句来实现:
varresult=newList(); foreach(vars1inlist1) foreach(vars2inlist2) result.Add($"{s1}{s2}"); //result=["a1b1","a1b2","a1b3","a2b1","a2b2","a2b3"]
改用SelectMany实现:
varresult=list1.SelectMany(x=>list2.Select(y=>$"{x}{y}")); //result=["a1b1","a1b2","a1b3","a2b1","a2b2","a2b3"]
具有挑战性的问题来了,如何对N个集合进行迪卡尔积运算呢,比如有这样的集合数据:
vararrList=newList{ newstring[]{"a1","a2"}, newstring[]{"b1","b2","b3"}, newstring[]{"c1"}, //... };
如何对上面的arrList中的各个集合进行两两组合呢?在电商业务尤其是零售业务中的产品组合促销中这种需求很常见。
下面是一个使用SelectMany的实现,需要用到递归:
classProgram { staticvoidMain(string[]args) { vararrList=newList{ newstring[]{"a1","a2"}, newstring[]{"b1","b2","b3"}, newstring[]{"c1"}, //... }; varresult=Recursion(arrList,0,newList ()); result.ForEach(x=>Console.WriteLine(x)); } staticList Recursion(List list,intstart,List result) { if(start>=list.Count) returnresult; if(result.Count==0) result=list[start].ToList(); else result=result.SelectMany(x=>list[start].Select(y=>x+y)).ToList(); result=Recursion(list,start+1,result); returnresult; } }
输出:
a1b1c1
a1b2c1
a1b3c1
a2b1c1
a2b2c1
a2b3c1
类似这种集合的迪卡尔积运算操作,也可以用LINQtoObject来代替SelectMany实现:
result=result.SelectMany(x=>list[start].Select(y=>x+y)).ToList(); //等同使用扩展方法: result=(fromainresultfrombinlist[start]selecta+b).ToList();
LINQtoObject比扩展方法看上去易读性更好,但写起来扩展方法更方便。
Aggregate聚合
Aggregate扩展方法可以对一个集合依次执行类似累加器的操作,就像滚雪球一样把数据逐步聚集在一起。比如实现从1加到10,用Aggregate扩展方法就很方便:
int[]numbers={1,2,3,4,5,6,7,8,9,10}; intsum=numbers.Aggregate((prevSum,current)=>prevSum+current); //sum=55
我们来解析一下它的执行步骤
- 第一步,prevSum取第一个元素的值,即prevSum=1
- 第二步,把第一步得到的prevSum的值加上第二个元素,即prevSum=prevSum+2
- 依此类推,第i步把第i-1得到的prevSum加上第i个元素
再来看一个字符串的例子加深理解:
string[]stringList={"Hello","World","!"}; stringjoinedString=stringList.Aggregate((prev,current)=>prev+""+current); //joinedString="HelloWorld!"
Aggregate还有一个重载方法,可以指定累加器的初始值。我们来看一个比较综合的复杂例子。假如我们有如下1-12的一个数字集合:
varitems=newList{1,2,3,4,5,6,7,8,9,10,11,12};
现在我们想做如下计算:
- 计算集合元素的总数个数
- 计算值为偶数的元素个数
- 收集每第4个元素
当然通过普通的循环遍历也可以实现这三个计算,但使用Aggregate会更简洁,下面是Aggregate的实现:
varresult=items.Aggregate(new{Total=0,Even=0,FourthItems=newList()}, (accum,item)=> new { Total=accum.Total+1, Even=accum.Even+(item%2==0?1:0), FourthItems=(accum.Total+1)%4==0?newList (accum.FourthItems){item}:accum.FourthItems } ); //result: //Total=12 //Even=6 //FourthItems=[4,8,12]
这里为了简单起见使用匿名类型作为累加器的初始值,由于匿名类型的属性是只读的,所以在累加的过程都new了一个新对象。如果初始值使用的是自定义类型,那累加时就不需new新对象了。
Jion关联查询
和SQL查询一样,LINQ同样支持InnerJoin、LeftJoin、RightJoin、CrossJoin和FullOuterJoin,有时候你可能看到不同的写法,其实是同一个意思,比如LeftOuterJoin就是LeftJoin,Join是InnerJoin省略了Inner等。
假设我们有下面两个集合,分别表示左边的数据和右边的数据。
varfirst=newList(){"a","b","c"};//左边 varsecond=newList (){"a","c","d"};//右边
下面以此数据为例来演示各种关联查询。
InnerJoin
varresult=fromfinfirst joinsinsecondonfequalss selectnew{f,s}; //等同使用扩展方法: varresult=first.Join(second, f=>f, s=>s, (f,s)=>new{f,s}); //result:{"a","a"} //{"c","c"}
LeftJoin
varresult=fromfinfirst joinsinsecondonfequalssintotemp fromtintemp.DefaultIfEmpty() selectnew{First=f,Second=t}; //或者: varresult=fromfinfirst fromsinsecond.Where(x=>x==f).DefaultIfEmpty() selectnew{First=f,Second=s}; //等同使用扩展方法: varresult=first.GroupJoin(second, f=>f, s=>s, (f,s)=>new{First=f,Second=s}) .SelectMany(temp=>temp.Second.DefaultIfEmpty(), (f,s)=>new{First=f.First,Second=s}); //result:{"a","a"} //{"b",null} //{"c","c"}
RightJoin
varresult=fromsinsecond joinfinfirstonsequalsfintotemp fromtintemp.DefaultIfEmpty() selectnew{First=t,Second=s}; //其它和LeftJoin类似 //result:{"a","a"} //{"c","c"} //{null,"d"}
CrossJoin
varresult=fromfinfirst fromsinsecond selectnew{f,s}; //result:{"a","a"} //{"a","c"} //{"a","d"} //{"b","a"} //{"b","c"} //{"b","d"} //{"c","a"} //{"c","c"} //{"c","d"}
FullOuterJoin
varleftJoin=fromfinfirst joinsinsecondonfequalssintotemp fromtintemp.DefaultIfEmpty() selectnew{First=f,Second=t}; varrightJoin=fromsinsecond joinfinfirstonsequalsfintotemp fromtintemp.DefaultIfEmpty() selectnew{First=t,Second=s}; varfullOuterJoin=leftJoin.Union(rightJoin);
根据多个键关联
在SQL中,表与表进行关联查询时on条件可以指定多个键的逻辑判断,用and或or连接。但C#的LINQ不支持and关键字,若要根据多键关联,需要把要关联的键值分别以相同的属性名放到匿名对象中,然后使用equals比较两个匿名对象是否相等。示例:
varstringProps=typeof(string).GetProperties(); varbuilderProps=typeof(StringBuilder).GetProperties(); varquery= fromsinstringProps joinbinbuilderProps onnew{s.Name,s.PropertyType}equalsnew{b.Name,b.PropertyType} selectnew { s.Name, s.PropertyType };
以上均使用两个集合做为示例,LINQ关联查询也支持多个集合关联,就像SQL的多表关联,只需往后继续追加join操作即可,不再累述。
LINQ关联查与SQL相似,但使用上有很大区别。LINQ关联查询的用法有很多,也很灵活,不用刻意去记住它们,只要熟悉简单常用的,其它的在实际用到的时候再查询相关文档。
Skip&Take分页
Skip扩展方法用来跳过从起始位置开始的指定数量的元素读取集合;Take扩展方法用来从集合中只读取指定数量的元素。
varvalues=new[]{5,4,3,2,1}; varskipTwo=values.Skip(2);//{3,2,1} vartakeThree=values.Take(3);//{5,4,3} varskipOneTakeTwo=values.Skip(1).Take(2);//{4,3}
Skip与Take两个方法结合即可实现我们常见的分页查询:
publicIEnumerableGetPage (thisIEnumerable collection,intpageNumber,intpageSize) { intstartIndex=(pageNumber-1)*pageSize; returncollection.Skip(startIndex).Take(pageSize); }
使用过EF(Core)的同学一定很熟悉。
另外,还有SkipWhile和TakeWhile扩展方法,它与Skip和Take不同的是,它们的参数是具体的条件。SkipWhile从起始位置开始忽略元素,直到匹配到符合条件的元素停止忽略,往后就是要查询的结果;TakeWhile从起始位置开始读取符合条件的元素,一旦遇到不符合条件的就停止读取,即使后面还有符合条件的也不再读取。示例:
SkipWhile:
int[]list={42,42,6,6,6,42}; varresult=list.SkipWhile(i=>i==42); //result:6,6,6,42
TakeWhile:
int[]list={1,10,40,50,44,70,4}; varresult=list.TakeWhile(item=>item<50).ToList(); //result={1,10,40}
Zip拉链
Zip扩展方法操作的对象是两个集合,它就像拉链一样,根据位置将两个系列中的每个元素依次配对在一起。其接收的参数是一个Func实例,该Func实例允许我们成对在处理两个集合中的元素。如果两个集合中的元素个数不相等,那么多出来的将会被忽略。
示例:
int[]numbers={3,5,7}; string[]words={"three","five","seven","ignored"}; IEnumerablezip=numbers.Zip(words,(n,w)=>n+"="+w); foreach(stringsinzip) { Console.WriteLine(s); }
输出:
3=three
5=five
7=seven
OfType和Cast类型过滤与转换
OfType用于筛选集合中指定类型的元素,Cast可以把集合转换为指定类型,但要求源类型必须可以隐式转换为目标类型。假如有如下数据:
interfaceIFoo{} classFoo:IFoo{} classBar:IFoo{} varitem0=newFoo(); varitem1=newFoo(); varitem2=newBar(); varitem3=newBar(); varcollection=newIFoo[]{item0,item1,item2,item3};
OfType示例:
varfoos=collection.OfType();//result:item0,item1 varbars=collection.OfType ();//result:item2,item3 varfoosAndBars=collection.OfType ();//result:item0,item1,item2,item3 //等同于使用Where varfoos=collection.Where(item=>itemisFoo);//result:item0,item1 varbars=collection.Where(item=>itemisBar);//result:item2,item3
Cast示例:
varbars=collection.Cast();//InvalidCastException异常 varfoos=collection.Cast ();//InvalidCastException异常 varfoosAndBars=collection.Cast ();//OK
ToLookup索引式查找
ToLookup扩展方法返回的是可索引查找的数据结构,它是一个ILookup实例,所有元素根据指定的键进行分组并可以按键进行索引。这样说有点抽象,来看具体示例:
string[]array={"one","two","three"}; //根据元素字符串长度创建一个查找对象 varlookup=array.ToLookup(item=>item.Length); //查找字符串长度为3的元素 varresult=lookup[3]; //result:one,two
再来一个示例:
int[]array={1,2,3,4,5,6,7,8}; //创建一个奇偶查找(键为0和1) varlookup=array.ToLookup(item=>item%2); //查找偶数 vareven=lookup[0]; //even:2,4,6,8 //查找奇数 varodd=lookup[1]; //odd:1,3,5,7
Distinct去重
Distinct方法用来去除重复项,这个容易理解。示例:
int[]array={1,2,3,4,2,5,3,1,2}; vardistinct=array.Distinct(); //distinct={1,2,3,4,5}
简单类型的集合调用Distinct方法使用的是默认的比较器,Distinct方法用此比较器来判断元素是否与其它元素重复,但对于自定义类型要实现去重则需要自定义比较器。示例:
publicclassIdEqualityComparer:IEqualityComparer{ publicboolEquals(Personx,Persony)=>x.Id==y.Id; publicintGetHashCode(Personp)=>p.Id; } publicclassPerson { publicintId{get;set;} publicstringName{get;set;} } classProgram { staticvoidMain(string[]args) { varpeople=newList (); vardistinct=people.Distinct(newIdEqualityComparer()); } }
ToDictionary字典转换
ToDictionary扩展方法可以把集合IEnumerable
IEnumerableusers=GetUsers(); Dictionary usersById=users.ToDictionary(x=>x.Id);
如果不用ToDictionary,你需要这样写:
IEnumerableusers=GetUsers(); Dictionary usersById=newDictionary (); foreach(Useruinusers) { usersById.Add(u.Id,u); }
上面ToDictionary返回的字典数据中的值是整个元素,你也可以通过它的第二个参数来自定义字典的值。示例:
DictionaryuserNamesById=users.ToDictionary(x=>x.Id,x=>x.Name);
你也可以为转换的字典指定其键是否区分大小写,即自定义字典的IComparer,示例:
DictionaryusersByCaseInsenstiveName=users.ToDictionary(x=>x.Name, StringComparer.InvariantCultureIgnoreCase); varuser1=usersByCaseInsenstiveName["liam"]; varuser2=usersByCaseInsenstiveName["LIAM"]; user1==user2;//true
注意,字典类型要求所有键不能重复,所以在使用ToDictionary方法时要确保作为字典的键的元素属性不能有重复值,否则会抛出异常。
其它常见扩展方法
LINQ还有很多其它常见的扩展方法,大家在平时应该用的比较多,比如Where、Any、All等,这里也选几个简单举例介绍一下。
Range和Repeat
Range和Repeat用于生成简单的数字或字符串系列。示例:
//生成1-100的数字,即结果为[1,2,...,99,100] varrange=Enumerable.Range(1,100); //生成三个重复的字符串“a”,即结果为["a","a","a"] varrepeatedValues=Enumerable.Repeat("a",3);
Any和All
Any用来判断集合中是否存在任一一个元素符合条件,All用来判断集合中是否所有元素符合条件。示例:
varnumbers=newint[]{1,2,3,4,5}; boolresult=numbers.Any();//true boolresult=numbers.Any(x=>x==6);//false boolresult=numbers.All(x=>x>0);//true boolresult=numbers.All(x=>x>1);//false
Concat和Union
Concat用来拼接两个集合,不会去除重复元素,示例:
Listfoo=newList {1,2,3}; List bar=newList {3,4,5}; //通过Enumerable类的静态方法 varresult=Enumerable.Concat(foo,bar).ToList();//1,2,3,3,4,5 //通过扩展方法 varresult=foo.Concat(bar).ToList();//1,2,3,3,4,5
Union也是用来拼接两个集合,与Concat不同的是,它会去除重复项,示例:
varresult=foo.Union(bar);//1,2,3,4,5
GroupBy分组
GroupBy扩展方法用来对集合进行分组,下面是一个根据奇偶进行分组的示例:
varlist=newList(){1,2,3,4,5,6,7,8,9}; vargrouped=list.GroupBy(x=>x%2==0); //grouped:[1,3,5,7,9]和[2,4,6,8]
还可以根据指定属性进行分组:
publicclassPerson { publicintAge{get;set;} publicstringName{get;set;} } varpeople=newList(); varquery=people .GroupBy(x=>x.Age) .Select(g=>{Age=g.Key,Count=g.Count()});
DefaultIfEmpty空替换
在上面的关联查询中我们使用了DefaultIfEmpty扩展方法,它表示在没有查询到指定条件的元素时使用元素的默认值代替。其实DefaultIfEmpty还可以指定其它的默认值,示例:
varchars=newList(){"a","b","c","d"}; chars.Where(s=>s.Length>1).DefaultIfEmpty().First();//返回null chars.DefaultIfEmpty("N/A").FirstOrDefault();//返回"a" chars.Where(s=>s.Length>1).DefaultIfEmpty("N/A").FirstOrDefault();//返回"N/A"
SequenceEqual集合相等
SequenceEqual扩展方法用于比较集合系列各个相同位置的元素是否相等。示例:
int[]a=newint[]{1,2,3}; int[]b=newint[]{1,2,3}; int[]c=newint[]{1,3,2}; boolresult1=a.SequenceEqual(b);//true boolresult2=a.SequenceEqual(c);//false
最后
还有一些常用和简单的扩展方法就不举例了,比如OrderBy(排序)、Sum(求和)、Count(计数)、Reverse(反转)等,同时欢迎大家补充本文遗漏的强大或好用的LINQ语法糖。
作者:精致码农
出处:http://cnblogs.com/willick
联系:liam.wang@live.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如有问题或建议,请多多赐教,非常感谢。
以上就是全面分析c#LINQ的详细内容,更多关于c#LINQ的资料请关注毛票票其它相关文章!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。