由于工作的忙碌,确实很久没时间来写点什么了。随着.net的使用深入,越来越觉得Linq是个伟大的发明。竟可以将复杂的逻辑变得如此简单、明了。使得我们可以对他如此爱不释手,当他与数据库结合的时候,更是产生了Linq to SQL以至于现在更为广泛通用的Entity Framework。 今天我们就来回归语言本身,来谈谈Linq吧。
Linq是Language Integrated Query的缩写,意思是语言集成查询,顾名思义,是用于查询的。其本质是针对如实现了IEnumerable(可枚举)接口的可查询的数据集提供方便的查询。首先,它实现了针对任何可查询集合的类似于sql语句的查询语法(from in where orderby select 语句);其次就是,它实现了针对可枚举对象的一系列扩展方法。相比较而言,扩展方法的使用频度远远多于查询语法,尤其是当linq to sql和.net3.5版本的entity出现之后,开发者们更是发现,使用扩展方法人们终于摆脱了sql语句,并且可以做到在执行数据查询时,不用再去关心到底连接的数据库究竟是什么。
下面,我们就来看看Linq提供的扩展方法,给我们带来的魔法世界吧。
首先,要对Expression有所了解。而Expression又是一阵套微软提供的官方库,同其他微软建立起来的概念一样,会有一堆晦涩的概念,要从头到尾解释一遍太费时。我们其实只需要对其中的一点有所了解就好了,那就是Lambda表达式。Lambda表达式并不是微软特有的概念,他的本质其实就是定义一个匿名的方法(匿名委托),从而简化对参数为Func或Action或Delegate等方法的调用。.net中的Lambda形式形如:
(i,j,k)=>{
//dosomething with i,j,k
//return something(or not);
}
这里i,j,k为参数,不需要指定参数类型,因为调用是会自动判断,也不需指定返回类型,如果有return xxx;语句,.net会通过return语句判断返回类型(Func);如果没有或者只有return; 则.net会判断为没有返回值(Action)。这里的参数列表,如果没有参数可以用()代替,如果只有一个参数则可以省略()。Lambda还有更为简化的写法:
i=>i.Name
这条语句等同于
i=>{
return i.Name;
}
了解这些是非常必要的,因为Linq所提供的扩展方法,几乎全部是以委托为参数的,这时候就是Lambda大显身手的时候了。
下面我们来看看标准的Linq吧(针对数据库的Linq是单独实现的,虽然实现了看起来方法名和参数都相同,但是实现方法却大大不同)。在System.Linq命名控件下,实现了针对IEnumareble的一大堆扩展方法(.Where,.Select,.Skip,.OrderBy,.First等等等等)。这些方法究竟可以带来怎样的便利,我们看几个例子就知道了:
例子1:
对于一个int数组(Array) a,我想要找出所有的数值大于60的值,并且作为新的int数组返回,如果不用Linq,是不是要写一个如下的方法:
private int[] getScores(int [] a){
List<int> b= new List<int>();
foreach(var item in a){
if (item > 60){
b.Add(item);
}
}
int [] c = new int[b.Count];
for(var i=0;i<c.Length;i++){
c[i]=b[i];
}
return c;
}
看出来有多复杂没有。那么首先让我们看看Linq实现的很牛的扩展方法,可以在常用的各种列表容器类型相互转换,那就是ToList(),ToArray(),ToDictionary等等。如果上述方法,针对中间变量的List,使用ToArray,则可以产生如下简化:
private int[] getScores(int [] a){
List<int> b= new List<int>();
foreach(var item in a){
if (item > 60){
b.Add(item);
}
}
return b.ToArray();
}
或者再稍微改造下:
private IEnumerable<int> getScores(int [] a){
foreach(var item in a){
if (item > 60){
yield return item;
}
}
}
getScores(a).ToArray(); //调用
这样够简单了吗? 显然不是,还要自己遍历来做判断。而Linq给我们提供了可以自动遍历并且判断是否满足条件的扩展方法,那就是马上要隆重介绍的.Where()方法。.Where方法接受一个返回值为bool的委托类型,而给该委托传递的参数,为IEnumareble<T>中的T,返回一个IQueryable(可查询)接口类型的数据,至于这个IQueryable到底是什么,我们不需要关心,只需要在做完所有的操作后,将其转化成我们需要的类型即可(调用ToArray,ToList),或者干脆直接使用枚举器来访问(foreach语句)。我们看看用.Where来实现之前的操作要怎么写:
a.Where(i => i > 60).ToArray();
是的,真的只有一行,就解决了上面一大堆语句才能实现的功能。之前我们用List作为过渡用的临时类型,但在linq中,我们不需要知道过渡类型是什么,只需在最好转换为我们需要的类型即可。
那么问题复杂一点:
例子2:
有一个类,定义如下:
internal class student{
public int id;
public string name;
public int score;
}
现在有一个List<student>类型的对象students,要求:查找出所有的score在60-80之间的,name中包含王的student的name的列表,并且按照id从大到小排序,返回的是List<string>类型。实现如下:
students.Where(i => i.score > =60 && i.score <= 80).Where(i.name.Contains("王")).OrderBy(i => i.id).Select(i => i.name).ToList();
看着很长,我们来一段一段看。
第一个Where是筛选出score>=60且score<=80的所有student,第二个Where是在上面筛出来的student中再次筛查name中包含“王”的,这里注意:Contains也是linq的扩展方法,意思是包含,返回为bool,表示是否包含,对于string,也是IEnumerable的,string的Contains就是说该string中是否包含指定的子串。
这里看到,.Where返回的是IQueryable,该接口同样来源于IEnumerable。也就是说,Linq可以连缀。这是一个很方便的特性。
这里也要重点讲一下,.Where语句的连缀,可以视为两个Where之间的条件用&&连接,但是实际上效率却并不一样,每次Where生成了新的IEnumerable,如果是两次Where,则生成了两次。所以不如将所有条件写在一个Where里然后用&&链接的效率高(这里有个例外,就是对于Linq To Entity等,由于各扩展方法只是拼接sql查询字串,所以效率并无差别)。
然后是.OrderBy(),这个语句接受的是一个keySelector,就是指定一个返回的object,在排序时将使用这个object来比较大小。
紧接着是.Select(),这个语句很神奇,后面会详细介绍,他是指定返回的类型和值,也是接受一个selector,与其他的方法不同,这个方法执行后,后面返回的IEnumerable<T>中T的类型会发生变化。这里是返回了name字段,所以以后的类型将变成IEnumerable<string>
最后是ToList方法,将中间类型的列表专为List<列表项类型>类型
这里需要注意的是Linq的方法的最终结果是顺序有关系的,这里先.Where 和先.OrderBy似乎没有什么不同,但是就效率而言,先.Where是更高的,而且,如果先Select再OrderBy的话,会因为Select之后没有了id字段而出错(select之后的类型已经变为IEnumerable<string>了),而如果有.Skip()或者.Take()等方法加入的话,查询出来的结果也是会不同的。
这里引出了我们即将重点介绍的Select方法。
Select方法是选择一个新的对象作为列表元素,从而生成新的列表。这里,新的列表可以是任何一种类型。如果返回的是列表中的原始类型,则类型不变,否则列表项就会变为新的类型。如果返回的是源列表项的字段,返回的类型就是IEnumerable<字段的类型>,否则,可以使用new 来生成一个新的类的对象用于指定列表项的类型,甚至可以生成一个匿名的新类型(使用new { key=value,... }),来返回一个匿名类型的列表(这里所说的列表是指IEnumerable对象)。
这样的话,其实Select中可以是很复杂的语句来生成一个新的列表对象。下面将举例说明:
(后面为了讨论方便,我将省略最后一步的类型转换(.ToList,.ToArray等))
例子3:
对于一个string a,可以确保他是由数字的id组合而成,并且由“,”分隔,形如 1,2,3,20,现在要将它转换为int[]数组,Linq如下:
a.Split(',').Select(i => int.Parse(i));
没错,就这么多,先将a用“,”分割成string 数组,然后对这个string数组,我们用Select对列表中的每一项(string类型)转换成int类型,然后返回,最终就产生一个int类型的列表
例子4:
对于一个string数组,保存用户的真实姓名(保证所有string不为空或null),我要显示在web页上之前做一些处理,以保护用户的隐私,这里的要求是,对于只有一个字符的,显示该字符;对于超过一个字符的,第一个字符显示应该显示的字符,其余字符都用*代替,除第一个字符外有多少个字符,就显示多少个*,则可以用下面的Linq实现:
.Select(i => i[0] + string.Join("", i.Skip(1).Select(j => "*")));
这里主要到之前提过的,string也是IEnumerable的,枚举项元素是char类型的。
这里引入了Skip方法,这个方法的参数是整数,表示跳过多少个元素(列表项)。该方法经常会与Take方法一起出现,后者表示最多取多少个元素(列表项),这样返回的列表就不会超过指定的整数个元素。
这里是使用string的第一个元素(char)去拼接另一个字串,这个字串的产生方法是用源string去跳过第一项(0号)之后,对每个元素(char)不管是什么,直接返回"*"字串(Select中没有返回与参数j有关的内容),所返回的的IEnumerable<char>再拼接起来。是不是很精妙?
再来看个返回匿名类型的例子:
例子5:
对于DateTime数组,我们希望返回3中不同的ToString形式:日期(yyyy年MM月dd日),时间(HH时mm分),最后是星期(简单的返回DayOfWeek枚举的ToString)
.Select(i => new { date = i.ToString("yyyy年MM月dd日"), time = i.Tostring("HH时mm分"), weekday = i.DayOfWeek.ToString() })
这里使用了匿名对象,该对象包含三个字段:date,time,weekday,都是string类型,select返回的就是该匿名对象的列表。
(注:文章中IEnumerable等一般指IEnumerable<T>等)
在下一篇文章中,我将会详细描述Linq to entity的用法,以及它与普通Linq的区别。