Linq学习总结之查询运算符笔记
由于本人的工作内容涉及大批量数据的分析与处理,经常会需要对各种List进行分析处理,所以慢慢的就发现Linq真的太好用了,于是就系统的把Linq教程看了一遍(主要参考了张子阳《.Net之美》中的内容。)具体Linq是什么,我就不赘述了,各种教程里面都会有介绍, 下面我就只针对Linq各种常用的查询运算符进行解释,并给出简单的demo.
Linq的主要用途就是对数据源进行查询,最常见的数据源就是集合。例如string[],int[],List<T>,ArrayList等等,不论数据源的类型是什么,Linq的查询方式都是相同的,本文就以最简单的List<Object>为例。
首先定义我们Linq操作的object类型,先定义一个Point类型,它包含Name属性,X属性(X轴坐标点),Y属性(Y轴坐标点)。
Point类:
class Point :
{
public string Name { get; set; }
public int X { get; set; }
public int Y { get; set; }
public Point(string name, int x, int y)
{
Name = name;
X = x;
Y = y;
}
public override string ToString()
{
return string.Format("Point {0}: X-{1},Y-{2}", Name, X, Y);
}
}
Program类:
class Program
{
static void Main(string[] args)
{
Point a = new Point("A", 1, 2);
Point b = new Point("B", 1, 2);
Point c = new Point("C", 1, 3);
Point d = new Point("D", 2, 3);
Point e = new Point("E", 1, 2);
Point f = new Point("F", 2, 3);
Point g = new Point("G", 1, 2);
var pointList = new List<Point> { e, d, c, b, a };
var otherPointList = new List<Point> {a,b,f,g };
}
}
(一) Select()
Select()运算符理解起来有点像数据库中的视图,可以进行对象的转换,我们可以从定义的List里面去select出我们想要的东西。
var nameList = pointList.Select(p => p.Name);
Console.WriteLine(string.Join(",",nameList));
这样通过select操作,我们就把我们pointList里面所有point对象的Name属性给取出来了,输出结果是A,B,C,D,B。另外,Select也可以为我们创建新的匿名类型,例如:
var xPointList = pointList.Select(p => new { XName = p.Name, XValue = p.X });
foreach(var xPoint in xPointList)
Console.WriteLine(xPoint.XName+":"+xPoint.XValue);
上面这段代码,我们就把Point对象中的Name属性和X属性单独select出来组成了一个新的匿名类(X轴类,记录X轴点的XName,已经X轴点的值XValue)。输出结果如下:
(二) Where()
Where()运算符用于对集合的筛选,接受Func<TSource,int,bool>委托,其中int参数代表的是集合中元素在集合中的***index,index从0开始。
例如,我们想从pointList中筛选第二个到第四个的对象出来。那么代码如下:
var myList = pointList.Where((p, index) => index > 1 && index < 5);
那么,如果我想在第二个到第四个中选择Y轴坐标点大于2的改怎么做呢?其实只需要在where条件语句里增加一个条件就行了,代码如下:
var myList = pointList.Where((p, index) => index > 1 && index < 5 && p.Y > 2);
当然,如果你的where条件完全不关心元素的index,就可以省略掉index参数,代码如下:
var myList = pointList.Where(p => p.Y>2);
OfType()也是筛选,不过不同的是,Oftype()定义在IEnumberable上,而不是IEnumerable<T>,所以它的作用就是从非泛型集合中筛选出目标类型的成员并返回IEnumberable<T>集合。下面用ArrayList举例,从一个ArrayList中筛选出我们定义的Point类。
ArrayList list = new ArrayList();
list.Add(11);
list.Add("test");
list.Add(a);
list.Add("demo");
list.Add(b);
var myList = list.OfType<Point>();
通过Debug结果,我们可以看到我们通过OfType()运算符已经把ArrayList中的Point对象全部筛选出来了:(四) First(),FirstOrDefault(),Last(),LastOrDefault()
1、First:取序列中满足条件的第一个元素,如果没有元素满足条件,则抛出异常
2、FirstOrDefault:取序列中满足条件的第一个元素,如果没有元素满足条件,则返回默认值(对于可以为null的对象,默认值为null,对于不能为null的对象,如int,默认
值为0) First、FirstOrDefault的区别在于:当没有元素满足条件时,一个抛出异常,一个返回默认值。
Point pointA = pointList.First(p => p.Y == 3);
Point pointB = pointList.FirstOrDefault(p => p.Y == 3);
两个方法找到的都是同一个Point c {"C", 1, 3}注: Find方法和FirstOrDefault方法效果相同,都是返回满足条件的第一个元素,如果没有该元素,则返回null。
那么这两个扩展方法有什么不同?
1)Find方法是.net Framework2.0的,而FirstOrDefault是3.5的。
2)Find方法只能在List<T>上使用,而后者能更广泛应用在IEnemerable<T>上。
3)Find最终是建立在Array的查找之上,而在IEnemerable上的FirstOrDefault是使用foreach查找的。因此,Find速度会比FirstOrDefault快很多
结论:在List<T>上使用Find较速度较快,而其他IEnemrable<T>类型只能使用FirstOrDefault。
3. Last(),LastOrDefault()方法与First(),FirstOrDefault()类似,只不过一个从前开始查找,一个从后面开始查找,不再赘述。
(五) GroupBy()
GroupBy()返回的是一个IEnumerable<IGrouping<TKey,TSource>>类型,同时IGrouping<TKey,TSource>也是一个序列,只不过多了一个Key属性,所以GroupBy()的返回值可以简单的看成是一个由List组成的List。
例如我想通过Point的Name属性来给我的pointList进行group,那么代码如下:
var xValueGroups = pointList.GroupBy(p => p.X);
foreach (var group in xValueGroups)
{
Console.WriteLine(string.Format("Xvalue:{0}", group.Key));
foreach (var point in group)
Console.WriteLine(point.ToString());
Console.WriteLine();
}
运行结果如下:
那么如果我的查询条件不止一个属性该怎么办呢?
var valueGroups = pointList.GroupBy(p =>new { p.X,p.Y});
foreach (var group in valueGroups)
{
Console.WriteLine(string.Format("Xvalue:{0},Yvalue:{1}",group.Key.X,group.Key.Y));
foreach (var point in group)
Console.WriteLine(point.ToString());
Console.WriteLine();
}
运行结果如下:
(六) OrderBy(),ThenBy(),OrderByDescending(),ThenByDesceding().
OrderBy()利用选定的键值对序列进行排序,例如我们现在想对pointList按照X的大小排序,那么代码如下:
var sortList = pointList.OrderBy(p => p.X);
其实也不一定非要某一属性直接作为键值,也可以使用计算得出的值进行排序,例如:
var sortListA = pointList.OrderBy(p => p.X+p.Y); //按照坐标轴X,Y和值的大小排序
var sortListB = pointList.OrderBy(p => p.X%2);//按照坐标轴X除以2的余数大小排序
如果规则复杂一点,例如先按照X值排序,再按照Y值排序,该怎么办呢?这个时候就该ThenBy()出场了,代码如下:
var sortList = pointList.OrderBy(p => p.X).ThenBy(p => p.Y);
注:其实排序还可以通过Sort()方法实现。
使用Sort()方法实现的时候,需要Point类型继承IComparable接口,并且实现CompareTo()方法。例:
class Point : IComparable
{
public string Name { get; set; }
public int X { get; set; }
public int Y { get; set; }
public Point(string name, int x, int y)
{
Name = name;
X = x;
Y = y;
}
public override string ToString()
{
return string.Format("Point {0}: X-{1},Y-{2}", Name, X, Y);
}
public int CompareTo(object obj)
{
Point other = (Point)obj;
if (Name == other.Name)
{
if (X == other.X)
return Y.CompareTo(other.Y);
else
return X.CompareTo(other.X);
}
else
return Name.CompareTo(other.Name);
}
}
这个时候就可以直接使用pointList.Sort()方法来实现排序了。
注意:
首先解释一下Skip和Take都用来干嘛的。
1.Skip跳过指定数量的项,并获取剩余的项
2.Take提取制定数量的项
3.TakeWhile根据指定条件提出数据 (与While类型,但是区别是while在遇到不符合条件的元素时会继续查找序列中满足条件的下一个元素,而TakeWhile遇到不符合条
件的元素会直接返回前面找到的元素)
4.SkipWhile根据指定条件跳过数据 (SkipWhile是TakeWhile的反操作,它会一直跳过符合条件的元素,直到遇到第一个不符合条件的元素,然后返回该元素后面的所有
元素构成的序列)
代码示例:
var pointList = new List<Point> { a,b,c,d,e };
//获取pointList中的前三项,即获取point a,b,c
var resultList = pointList.Take(3);
//跳过前三项,获取后面的所有项,即point d,e
resultList = pointList.Skip(3);
//获取满足X=1的项,遇到第一个不满足条件的项就返回. 即point a,b,c (注意point e得不到,因为在遇到point d不满足条件就已经返回了)
resultList = pointList.TakeWhile(p=>p.X==1);
//一直跳过满足Y=2的项,遇到第一个不满足条件的项就返回后面的所有项. 即point c,d,e (注意point e不会被跳过,因为在遇到point c (Y=3)已经不满足条件就停止skip动作了)
resultList = pointList.SkipWhile(p => p.Y == 2);
Distinct()运算符用于去除序列中重复的元素,并返回其余所有的元素。但是重点是,元素的重复我们怎么去判断,基本类型元素当然不用操心,1==1,"AAB"=="AAB"这种
问题没有人会怀疑,那么对于我们自定义的对象该怎么去判断是否相等呢(默认是内存地址相同才判断相等)?下面我列出两种常用方法来实现自定义的判断对象是否相等(注:
实现判断对象是否相等对于序列的其他操作同样有意义,例如后面会提到的intersect(),Except(),Union()都需要先判断元素的相等性再执行操作,另外当对象当做Dictionary的
Key值使用时也是如此。)
1.) 重写对象的Equals()方法和GetHasCode()方法。
自定义的类,如我们的Point类都是继承于Object对象的,也就继承了Object的Equals和GetHasCode的方法,因此默认是内存地址相同才判断相等。那么如果想要实现自定义的
比较两个对象是否相等的话,就需要重写Object类的Equals()和GetHasCode()方法。代码如下:
class Point
{
public string Name { get; set; }
public int X { get; set; }
public int Y { get; set; }
public Point(string name, int x, int y)
{
Name = name;
X = x;
Y = y;
}
public override string ToString()
{
return string.Format("Point {0}: X-{1},Y-{2}", Name, X, Y);
}
//只需要保证Equals的两个对象拥有相同的HasCode即可,方法可以自己定义
public override int GetHashCode()
{
return Name.GetHashCode() + X.GetHashCode() * 2 + Y.GetHashCode() * 3;
}
//自定义只要两个点的名字一样,X,Y坐标轴的值一样就认为相等
public override bool Equals(object obj)
{
Point other = (Point)obj;
return Name == other.Name && X == other.X && Y == other.Y;
}
}
例如这个时候我们在pointList里面添加一个新元素h,但是这个point h的Name,X,Y都与point d相同。即 Point h = new Point("D", 2, 3); 这个时候pointList有6个元素a,b,c,d,e,h
在重写Equals和GetHasCode方法之前,我们调用 pointList.Distinct()方法返回的还是原来的6个元素,因为d,h的内存地址不一样,默认的判断会认为d和h不相等,所有distinct
方法没有起作用。那么,重写Equals和GetHasCode方法之后,我们再次执行Distince()方法会发现point h没有了,因为这个时候判断相等的依据是Name,X,Y 这3个元素是否相
等,因此point h被当做和point d重复,直接distinct掉了。
2.)创建一个PointComparer类,让它去实现IEqualityComparer<T>接口。
PointComparer类代码如下:
class PointComparer : IEqualityComparer<Point>
{
public bool Equals(Point x, Point y)
{
return x.Name == y.Name && x.X == y.X && x.Y == y.Y;
}
public int GetHashCode(Point obj)
{
return obj.Name.GetHashCode() + obj.X.GetHashCode() * 2 + obj.Y.GetHashCode() * 3;
}
}
然后再调用Distinct()的重载方法就可以实现与1.)相同的效果 (注:使用PointComparer类的时候,就不需要Point类去重写Equals和GetHasCode方法了)
var comparer = new PointComparer();
var resultList = pointList.Distinct(comparer);
(九) Reverse()
用于将序列中的元素逆序排列,例如 pointList.Reverse()就是将原先的序列a,b,c,d,e逆序成e,d,c,b,a
(十) Cast()
Cast()与OfType()类似,是定于在IEnumberable上的,它用于将非泛型序列转换为泛型序列,当序列中某一项不能成功转换时会抛异常。但是泛型使用Cast做类型转换的
用法我还没有完全摸清,以后再补充。
(十一) Intersect(),Except()
这两个运算符都是对两个序列进行操作,我们已pointList和otherPointList为例:(注:这里都是在Point对象重写了Equals和GetHasCode方法或者创建PointComparer基
础上实现的,参考Distinct()方法中的介绍)
var pointList = new List<Point> { a,b,c,d,e };
var otherPointList = new List<Point> {a,b,f,g };
1.) Intersect() 返回两个序列中相同的元素。例,下面的代码返回Point a, Point b //使用PointComparer类
var resultList = pointList.Intersect(otherPointList, comparer);
//重写Equals(),GetHasCode()方法
var resultList = pointList.Intersect(otherPointList);
2.)Except()返回第一个序列中有而第二个序列中没有的元素.例,下面的代码返回Point c,Point d,Point e //使用PointComparer类
var resultList = pointList.Except(otherPointList, comparer);
//重写Equals(),GetHasCode()方法
var resultList = pointList.Except(otherPointList);
(十二) Concat(),Union()
这两个运算符都是用来连接两个序列,但是不同的是Union()在连接的时候会剔除相同的项目(关于相同的判断,请参考Distinct中的介绍)
//返回序列a,b,c,d,e,a,b,f,g
var concatList = pointList.Concat(otherPointList).ToList();
//返回序列a,b,c,d,e,f,g
var unionList = pointList.Union(otherPointList, comparer).ToList();
该运算符是对两个序列中位置相同的两个元素进行操作,基于两个元素的结果返回一个新的元素,如果两个序列长度不相等,则以长度短的为准。例如
var indexList = new List<string> { "First: ", "Second: ", "Third: "};
var query = indexList.Zip(pointList, (x, y) => x + y.Name).ToList();
foreach(var item in query)
Console.WriteLine(item);
输出结果是:
First: A
Second: B
Third: C
(十四) SequenceEqual()
该运算符用来逐个的比较两个序列中的元素是否相同。即,序列元素数目相同,并且相同位置的元素也相同。(同样涉及元素比较,参考Distinct的介绍)
例:
bool result1 = pointList.SequenceEqual(otherPointList);// reture false
Point h = new Point("D", 2, 3);//Same as Point d
var pointListTag = new List<Point> { a, b, c, h, e };
bool result2 = pointList.SequenceEqual(pointListTag);//return true
(十五) Aggregate()
该运算符用户返回自定义的聚合。例:
//其中“Points in pointList:\r\n”是为聚合运算的初始值,即total的初始值。total是运算结果,lambda表达式是运算方法
string showAllPoints = pointList.Aggregate("Points in pointList:\r\n", (total, p) => total + p.ToString() + "\r\n");
Console.WriteLine(showAllPoints);
执行结果:
(十六) Single(),SingleOrDefaule()
这两个运算符与First(),FirstOrDefault()很相似,但是区别在于Single()和SingleOrDefaule()是要找序列中唯一满足条件的那一项,当满足条件的元素不止一个的时候,就会抛出异常。如果没有满足的项Single()会抛异常,SingleOrDefaule()返回默认值,这一点倒是与first(),FirstOrDefault()一样。
(十七) Contains(),All(),Any(),Exists()
这几个运算符其实都是对序列元素存在性的检查或校验。
Contains: 例如我们想判断pointList里面是不是存在一个Point h = new Point("D", 2, 3);
Point h = new Point("D", 2, 3);
bool contain = pointList.Contains(h);
这里运行的结果是ture,可能有很多小伙伴就会好奇为啥pointList里面明明只有a,b,c,d,e.为什么contains(h)会返回true呢?这里就涉及到之前在Distinct里面介绍的判断相等的方法
了,因为Point类重写了Equals()和GetHasCode方法,所以point h被判断与point d相等,所以contains()函数就返回true了。
All(): 例如我们判断是不是pointList里面所有的点的Y值都大于2
bool isAll = pointList.All(p => p.Y > 2);
很明显,结果是false.
Any(),Exists()
这两个运算符用法一样,区别在于Any()是随Linq引入的,Exists()方法在Linq出现之前就有了。
例如我们判断pointList里面是不是有Y值大于2的点:
bool isAny = pointList.Any(p => p.Y > 2);
bool isExists = pointList.Exists(p => p.Y > 2);
明显,这两个结果都是返回true.(十八) Count(),Max(),Min(),Average(),Sum().
Count():
显而易见,Count()方法就是返回序列中元素的个数,如果带lamda表达式,就是返回序列中满足条件的元素个数。
int totalCount = pointList.Count();// totalCount =5;
int count = pointList.Count(p => p.Y == 3);//count =2;
Max(),Min(),Average(),Sum()这几个运算符从字面意思就很容易理解,例子如下: //返回pointList中最大的Y值
int max = pointList.Select(p => p.Y).Max();//max =3
//返回pointList中最小的Y值
int min = pointList.Select(p => p.Y).Min();//min =2
//返回pointList中Y坐标的平均值
double average = pointList.Select(p => p.Y).Average();//average =2.4
//返回pointList中所有Y值的和
int sum = pointList.Select(p => p.Y).Sum();//sum =12
上一篇: LINQ之路(1):LINQ基础
下一篇: 初识LINQ 2