Linq
1、Linq
Linq
是.net core
中提供的一种简化的数据查询的技术。使用Linq
,可以实现几行代码就可以完成复杂的数据查询。
Linq
不仅可以对普通的.Net
集合进行查询,而且在Entiy Framework Core 中也被广泛的使用,所以必须熟练掌握``Linq
一、 Lambda表达式
1、委托复习
Lambda
表达式是C#中的语法,Lambda
表达式在LINQ
、ASP.NET Core等
很多场合都用得非常多.
要理解Lambda
表达式,先要说一下委托。
委托是一种可以指向方法的类型。
如下面的代码所示:
1 | MyDelegate d1 = SayEnglish; |
在上面的代码中,我们定义了委托MyDelegate
,并且制定了所指向的方法的参数类型与返回值类型。参数类型是int
,返回数据类型是strting
,所以我们可以看到,所指向的方法SayEnglish
和SayChinese
符合这个规定。
由于委托变量dl与d2
分别指向了SayEnglish
和SayChinese
两个方法,所以我们可以直接调用委托类型的变量,这时候,执行的就是变量指向的方法。
在.NET中定义了最多可达16个参数的泛型委托Action
(无返回值)和Func
(有返回值),因此一般我们不需要自定义委托类型,可以直接使用Action
或者Func
这两个委托类型。
1 | static string SayChinese(int age) |
在上面的代码中,定义了参数类型是int
,返回值类型为string
的委托变量fn1
.
指向了SayChinese
这个方法,该方法符合fn1
这个委托的要求。
下面通过委托变量fn1
完成了对SayChinese
这个方法的调用。
当然,委托不仅可以指向普通方法,也可以指向匿名方法,如下所示:
1 | Func<int, string> fn1 = delegate (int age) |
上面的委托变量fn1
指向了一个有int
参数,并且返回值是string
类型的匿名方法。
如果想传递多个参数,如下所示:
1 | Func<int, int, string> fn2 = delegate (int i, int j) |
在上面的代码中,委托变量 fn2
指向的匿名函数需要两个整型参数。
如果委托变量指向的函数,不需要返回值,可以通过Action
来定义委托变量,如下代码所示:
1 | Action<int, int> fn3 = delegate (int num1, int num2) |
2、Lambda
表达式写法
定义匿名函数也可以通过lambda
表达式的语法来完成。
Lambda 表达式”是一个匿名函数,它可以包含表达式和语句。可用于创建委托。
运算符 =>,该运算符读为“goes to”。
格式:(input parameters) => expression
如下代码所示:
1 | Func<int, int, string> fn = (int i, int j) => |
在上面的代码中,去掉了delegate
关键字,然后添加了=>
.用=>
作为定义方法体的关键字。
其实,这里也可以将参数
的类型省略,如下所示:
1 | Func<int, int, string> fn = ( i, j) => |
上面的代码省略了i,j
参数的类型,编译器会根据委托类型推断出参数的类型。
接下来还可以进一步简化这些代码。如果=>之后的方法体中只有一行代码,并且方法有返回值,那么还可以省略方法体的花括号及return关键字,如下代码所示:
1 | Func<int, int, string> fn = ( i, j) => $"两个数的和是{i + j}"; |
3、Lambda
简化规则
匿名方法用Lambda表达式改写还有如下简化的规则:
第一:如果一个方法没有返回值,并且方法体只有一行代码,也可以省略方法体的花括号。
关于这一点与上面的例子类似,只不过这里是没有返回值,所以需要使用Action
委托。
如下代码所示:
1 | Action<int, int> fn = (i, j) => Console.WriteLine($"两个数的和是{i + j}"); |
第二:
如果一个方法只有一个参数,那么Lambda表达式参数中的圆括号也可以省略.
如下代码所示:
1 | Func<int, int> fn = i => i + 6; |
下面我们再来看一个例子,体会一下,Lambda
表达式的应用。
该案例的要求:定义一个方法,实现将数组中大于10的数据过滤出来。
代码如下所示:
1 |
|
在上面的代码中,调用MyFilter
方法的时候,第二个参数传递的就是一个lambda
表达式,如果这里换成匿名函数,会变成如下的形式,如下所示:
1 | int[] arrs = { 10,23,1,2,56,12,55,6}; |
通过上面的代码,我们发现匿名函数的写法比较复杂,通过Lambda
表达式的写法更加的简单。
4、隐式类型var
在上面的代码中,我们知道MyFilter
方法的返回类型是泛型List<int>
,所以定义的接收变量results
的类型就是List<int>
.
写法上稍微复杂,这里完全可以通过var
来定义results
变量。如下所示:
1 | int[] arrs = { 10,23,1,2,56,12,55,6}; |
var
关键字会指示编译器根据初始化语句右侧的表达式推断变量的类型。
1 | var i = 6; |
通过ILSpy
反编译(对应的dll
文件)上面的代码后 ,得到的结果如下所示:
1 | internal class Program |
通过反编译的结果可以看到,编译器在编译的时候,确定了变量的类型。
5、匿名类
隐式类型var
在匿名类中使用的最多。
将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。 类型名由编译器生成。 每个属性的类型由编译器推断。var
用来初始化属性的表达式不能为 null、匿名函数
1 | var person = new { name = "zhangsan", age = 18 }; |
6、C#
扩展方法
什么是扩展方法?
简单的理解:扩展方法指的就是向现有的类型中添加新的方法。
扩展方法定义的规则?
(1) 扩展方法所在的类必须声明为static
。
(2) 扩展方法必须声明为public和static
(3) 扩展方法的第一个参数必须包含关键字this,并且在后面指定扩展的类的名称。
1 | Console.WriteLine("3".StringToInt() + 3);// 这里相当于将字符串3转成了整型3 |
通过上面的代码, 我们可以看到ExpandMethod
类是一个静态类,同时StringToInt
方法是一个public
的static
的方法同时该方法第一个参数必须添加this
关键字,同时指定了扩展的类型是string
这样,在字符串中可以直接调用StringToInt
方法,而StringToInt
方法在原有的string
类型中是没有的,现在为其扩展了该方法。
当然,这里,如果你想为扩展方法StringToInt
提供额外的参数也是可以的,如下代码所示:
1 | Console.WriteLine("3".StringToInt(6) + 3); // 在调用StringToInt方法的时候,传递了6这个参数,最终累加的结果是12 |
在Linq
中提供了很多关于集合类的扩展方法。而所有实现了IEnumerable<T>
这个接口的类都可以使用这些方法。
这些方法不是IEnumerable<T>
中的方法,而是以扩展方法的形式存在于System.Linq
命名空间的静态类中。
接下来我们开始讲解LINQ
中常用的集合类的扩展方法.
6.1 数据过滤
在Linq
中提供了where
这个扩展方法用于根据条件对数据进行过滤。下面看一下该where
方法的使用,如下代码所示:
1 | List<Person> list = new List<Person>() |
在上面的代码中,定义了Person
这个类,同时创建了list
这个集合,向该集合中添加了用户的信息。下面调用了where
这个扩展方法进行了过滤。
这里会将list
集合中每一个用户的信息取出来,然后根据Where
扩展方法中的lambda
表达式进行过滤,将符合条件的数据填充到了lists
中,这里的lists
的类型就是IEnumerable<T>
这个泛型,T
是什么类型呢?需要根据具体的过滤的数据来进行确定。
Where
方法的声明如下所示:
1 | public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate); |
可以看到Where
方法就是IEnumerable<TSource>
的扩展方法(第一个参数中指定了this
以及扩展的类型),当然Where
方法也是一个泛型方法,具体的类型和IEnumerable
的类型是一致的,表示具体过滤的数据类型。Where
方法的第二个参数是一个返回值为bool
类型的委托。
在source
这个集合参数中的每一项数据都会通过predicate
这个委托来进行测试过滤,如果某个元素通过predicate
委托执行的结果的返回值是true
,那么这个元素就会放到返回值中。所以说Where
这个扩展的方法的返回值就是通过predicate
委托进行数据过滤后的元素的集合。
问题:如果不使用Where
这个扩展方法,实现如上案例的要求应该怎样实现呢?
如下代码所示:
1 | List<Person> list = new List<Person>() |
通过对比发现,使用Where
这个扩展方法在进行数据过滤的时候更加的简单。
6.2 获取数据条数
获取数据的条数可以通过Count
这个扩展方法来完成,如下代码所示:
1 | List<Person> list = new List<Person>() |
问题:如果统计薪资大于2000的用户有多少,应该怎样进行过滤呢?
这里需要使用到Count
这个扩展方法的另外一种重载的方式。
1 | List<Person> list = new List<Person>() |
当然,有同学可能会想到如下的方式,如下代码所示:
1 | List<Person> list = new List<Person>() |
在上面的代码中,先通过Where
进行过滤,然后在调用Count
方法进行统计,最终的执行结果是一样的。
这里为什么调用了Where
方法以后,有可以调用Count
方法呢?
我们知道Where
方法返回的值类型是IEnumerable
类型,所以这里可以继续链式的调用其他的扩展方法。
注意:如果过滤条件返回的数据太多,超出了int
类型的最大值,可以调用LongCount
方法,该方法返回值的类型是long
类型。
LongCount
的用法和Count
的用法是一样的。
1 | long count=list.Where(p=>p.Salary>2000).LongCount(); |
6.3 Any
方法使用
Any
方法可以用来判断集合中是否至少有一条满足条件的数据,返回值类型为bool
.
如下代码所示:
1 | bool result = list.Any(p => p.Salary > 5000); |
运行上面的代码得到的结果是true
.
因为,在list
这个集合中有一条工资大于5000的记录。
如果,将集合修改成如下的形式:
1 | List<Person> list = new List<Person>() |
在上面的list
这个集合中,添加了一条工资大于5000的记录,然后运行程序,得到的结果也是true
.
这里我们可以得到一个关于Any
方法的结论:Any
方法只关心“有没有符合条件的数据”,而不关心符合条件的有几条数据,只要有符合条件的数据,返回的结果就是true
.
当然,这里我们也可以先执行Where
方法进行过滤,然后在调用Any
方法,如下所示:
1 | bool result = list.Where(p => p.Salary > 5000).Any(); |
当然,有同学可能想到上面的需求也可以通过Count
方法来实现,如下所示:
1 | bool result = list.Count(c => c.Salary > 5000) > 0; |
这里首先是通过Count
方法统计出工资大于5000
的记录数,然后在判断是否大于0.
但是,这里我们需要注意的就是:Count
方法在进行处理的时候,会先计算出满足条件的有几条数据,所以这里Count
会一直计算到最后一条记录才知道满足条件的数据条数,而Any
方法只关心“有没有符合条件的数据”,而不关心符合条件的有几条数据,因此在执行的时候,Any只要遇到一个满足条件的数据就停止继续向后检查数据。
所以使用Any实现的效率比用Count实现的更高。如果只是想判断数据是否存在,请使用Any方法。
6.4 获取一条数据
LINQ
中有4组获取一条数据的方法,分别是Single、SingleOrDefault、First和FirstOrDefault
。这4组方法的返回值都是符合条件的一条数据,每组方法也同样有两个重载方法,一个没有参数,另一个有一个Func<TSource,bool>predicate
参数。下面解释一下这4组方法的区别
1 | Single:如果确认有且只有一条满足要求的数据,那么就用Single方法。如果没有满足条件的数据,或者满足条件的数据多于一条,Single方法就会抛出异常 |
具体的代码示例如下所示:
下面先看Single
方法的使用
1 | List<Person> list = new List<Person>() |
下面是关于SingleOrDefault
方法的使用,如下所示:
1 | // SingleOrDefault:如果确认最多只有一条满足要求的数据,那么就用SingleOrDefault方法。如果没有满足条件的数据,SingleOrDefault方法就会返回类型的默认值。如果满足条件的数据多于一条,SingleOrDefault方法就会抛出异常 |
修改成以下的过滤条件:
1 | Person? p = list.SingleOrDefault(p=>p.Id==10); |
返回的结果就是null
1 | Person? p = list.SingleOrDefault(p => p.Id == 3); |
执行以上的代码会抛出异常,因为集合中Id
为3的记录有两条。
First
方法的使用
First:如果满足条件的数据有一条或者多条,First方法就会返回第一条数据;如果没有满足条件的数据,First方法就会抛出异常.
1 | Person p= list.First(); |
这里直接调用了First
这个扩展方法,这时候返回的就是List
集合中的第一条数据。
当然,也可以给First
方法传递过滤的条件,如下代码所示:
1 | Person p= list.First(p=>p.Age>20); |
我们知道,List
集合中存储的用户信息中,年龄大于20的有多条记录,而这时候First
方法返回的只是满足条件的第一条记录。
1 | Person p= list.First(p=>p.Age>60); // 这行代码会抛出异常。 |
在List
集合中,没有年龄大于60,所以执行以上代码会抛出异常。
FirstOrDefault
方法的使用
FirstOrDefault
:如果满足条件的数据有一条或者多条,FirstOrDefault
方法就会返回第一条数据;如果没有满足条件的数据,FirstOrDefaul
t方法就会返回类型的默认值。
1 | Person? p= list.FirstOrDefault(); |
在使用FirstOrDefault
方法的时候,如果没有指定过滤条件,默认会返回List
集合中第一条记录。
1 | Person? p= list.FirstOrDefault(p=>p.Age>20); |
我们知道,List
集合中存储的用户信息中,年龄大于20的有多条记录,而这时候FirstOrDefault
方法返回的只是满足条件的第一条记录。
1 | Person? p= list.FirstOrDefault(p=>p.Age>60); |
给以上给FirstOrDefault
方法指定的条件,找不到满足条件的记录,这时候返回的就是null
,所以这里需要进行判断。
6.5 排序
OrderBy
方法可以对数据进行正向排序,而OrderByDescending
方法则可以对数据进行逆向(倒序)排序
1 | Console.WriteLine("按照年龄正序排序"); |
以上代码是按照年龄正序排序
这里的OrderBy
方法返回的数据类型是:IOrderedEnumerable<TSource>
,而IOrderedEnumerable<TSource>
继承了IEnumerable<TElement>
,所以以上定义的items
的类型是IEnumerable<Person>
1 | IOrderedEnumerable<Person> items =list.OrderBy(p => p.Age); |
当然,这里定义成IOrderedEnumerable<Person>
类型也是可以的。
下面进行倒序排序
1 | IOrderedEnumerable<Person> items =list.OrderByDescending(p => p.Age); |
当然,如果在指定items
这个变量类型的时候感觉比较麻烦,可以直接使用var
.让编译器推断其类型。
我们知道在List
集合中,用户laoli
和maliu
的年龄是一样的,那这时候会按照什么规则进行排序呢?
这里会根据数据在List
集合中的顺序来决定。可以调整以上两个用户在list
集合的顺序来看一下过滤的结果。
这里有一个要求:先按照年龄升序排序,如果年龄相同,再按照工资降序排序。
1 | IOrderedEnumerable<Person> items =list.OrderBy(p => p.Age).ThenByDescending(a=>a.Salary); |
注意:这里使用了ThenByDescending
问题:先按照年龄降序排序,如果年龄相同,再按照工资升序排序
1 |
|
注意:这里使用了ThenBy
方法。
同时这里还需要注意的一点:千万不要写成:list.OrderBy(p=>p.Age).OrderByDescending(p=>p.Salary)
在以上的排序案例中,我们是针对List
这个集合中Person
数据进行排序的,但是如果要对数组中的数据进行排序应该怎样处理呢?
1 | int[] nums = { 3,5,1,23,67,21,10}; |
这里,我们可以总结出一点:针对OrderBy
和OrderByDescending
方法进行排序的时候,指定的排序条件不一定是对象中的某个属性,例如以上案例所示,所以说具体的排序条件需要根据具体的情况写任意的表达式。
下面的案例,是根据用户名中最后一个字母进行从大到小的排序,也就是降序排序。
1 | IOrderedEnumerable<Person> items = list.OrderByDescending(p => p.Name![p.Name!.Length - 1]); |
这里通过 p.Name![p.Name!.Length - 1]
获取的就是Name
这个属性中的最后一个字符。
!
表示断言,也就是这里不肯能会出现null
.
6.6 限制结果集
所谓的限制结果集,指的就是从集合中获取部分数据。其主要的应用场景就是分页查询。例如:从第2条开始获取3条数据。
这里主要有两个方法。
Skip(n)
方法用于跳过n
条数据.
Take(n)
方法用于获取n
条数据。
下面先来看一下Skip()
方法的使用
代码如下所示:
1 | List<Person> list = new List<Person>() |
在上面的代码中,使用了Skip(1)
,表示跳过第一条记录,最终获取到的是剩余的所有的记录。
下面再来看一下Take()
方法的使用。
1 | var items = list.Take(1); |
这里使用了Take(1)
表示的就是去取第一条,这里没有进行跳转。
如果想跳过第1条,取2条,应该怎样处理呢?这里可以将Take
方法与Skip
方法联合来使用。
如下代码所示:
1 | var items = list.Skip(1).Take(2); |
问题:先过滤出年龄大于等于30的用户,然后在按照年龄进行升序排序,排序完成后,跳过第1条,取2条记录。
1 | List<Person> list = new List<Person>() |
通过以上的代码可以体会出链式调用。
6.7 聚合函数
我们知道,SQL
中有Max
(最大值)、Min
(最小值)、Avg
(平均值)、Sum
(总和)、Count
(总数)等聚合函数。LINQ
中也有对应的方法,它们的名字分别是Max、Min、Average、Sum和Count
,这些方法也可以和Where、Skip、Take
等方法一起使用,如下代码所示:
1 | List<Person> list = new List<Person>() |
在前面我们也强调过,这些聚合函数可以和其他的扩展方法一起使用。
例如,这里有一个需求:统计出年龄大于等于30的用户中,最高工资是多少?
1 | double maxSalary=list.Where(p => p.Age >= 30).Max(p => p.Salary); |
如果集合是int
等值类型的集合,我们也可以使用没有参数的聚合函数,如下代码所示:
1 | int[] arr = { 56,12,31,67,89,2,3,55,59,90}; |
6.8 分组
LINQ
中支持类似于SQL
中的group by
实现的分组操作。GroupBy
方法用来进行分组
GroupBy
方法的声明比较复杂,如下:
1 | IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector); |
GroupBy
方法的参数keySelector
是分组条件表达式,GroupBy
方法的返回值为IGrouping<TKey,TSource>
类型的泛型IEnumerable。IGrouping
是一个继承自IEnumerable
的接口,IGrouping
中唯一的成员就是Key属性,表示这一组数据的数据项。由于IGrouping
是继承自IEnumerable
接口的,因此我们依然可以使用Count、Min、Average
等方法进行组内的数据聚合运算。
问题:根据年龄进行分组,然后计算组内的人数,平均工资等。
1 | List<Person> list = new List<Person>() |
当然,上面去写类型的时候,比较麻烦,所以这里我们都使用类型推断var
.
如下所示:
1 | var items= list.GroupBy(c => c.Age); |
我们知道IGrouping
也是一个集合,所以下面我们可以计算出这一组中相关的一些数据,例如,当前每一组有多少条记录,没一组中最大的工资等等。
如下代码所示:
1 | var items= list.GroupBy(c => c.Age); |
注意:IGrouping
是一个泛型类型,因此Key
的类型和分组条件表达式中值的类型一致,
在上面的代码中分组条件是int
类型的Age
,所以说代码中的item.Key
就是int
类型。
如果我们要根据性别Gender
进行分组,而Gender
的类型是bool
类型,所以说item.Key
的类型就是bool
类型。
6.9 投影操作
投影:可以对集合使用Select
方法进行投影操作,通俗来说就是把集合中的每一项逐项转换为另外一种类型,Select方法的参数是转换的表达式,它能够选择数据源中的元素,并指定元素的表现形式.
Select
方法与Sql
中的select
查询非常类似。
1 | select name,age from Person |
以上就是把Person
表中的name
和age
两个字段筛选出来,这就是投影。
1 | IEnumerable<int> ages = list.Select(p => p.Age); |
在上面的代码中,Select
方法把list
中每一项的Age
通过投影操作提取出来,因为Age
是int
类型的,所以Select
方法的返回值就是IEnumerable<int>
类型.
下面筛选出用户的姓名
1 | IEnumerable<string> names = list.Select(p => p.Name!); |
如果想将用户名和年龄都筛选出来呢?
1 | IEnumerable<string> names = list.Select(p => p.Name!+","+p.Age); |
下面,筛选出工资大于3000的用户的性别:
1 | IEnumerable<string> names = list.Where(p=>p.Salary>3000).Select(p => p.Gender?"男":"女"); |
在Select
方法中也可以使用匿名类型.
用Select
方法从list
的每一项中提取出Name
、Age
属性的值,并且把Gender
转换为字符串,Select
方法的返回值是一个匿名类型的IEnumerable
类型,因此我们必须用var
声明变量类型。
1 | var items = list.Select(p => new { Name = p.Name, Age = p.Age, Gender = p.Gender ? "男" : "女" }); |
也可以与Group by
一起使用。
1 | var items = list.GroupBy(p => p.Age).Select(c => new { Age = c.Key,MaxSalary=c.Max(a=>a.Salary),count=c.Count() }); |
1 | var items = list.Select(a => new { Age = a.Age, UserName = a.Name }).GroupBy(a => a.Age); |
6.10 链式调用
通过前面的学习,我们知道linq
中是可以进行链式调用的。这是因为像Where、Select、OrderBy、GroupBy、Take、Skip
等方法的返回值都是IEnumerable<T>
类型,因此它们是可以被链式调用的.
下面我们再来看一个例子:
获取Id>2的
数据,再按照Age
分组,并且把分组按照Age
排序,然后取出前2
条,最后投影取得年龄、人数、平均工资
1 | var items = list.Where(p => p.Id > 2).GroupBy(g => g.Age).OrderBy(a => a.Key).Take(2).Select(b => new { Age = b.Key, count = b.Count(), avgSalary = b.Average(c => c.Salary) }); // 注意类型,这里的c类型是Person |
6.11 LINQ
的另一种写法
前面我们学到的使用Where、OrderBy、Select
等扩展方法进行数据查询的写法叫作“方法语法”。除此之外,LINQ
还有另外一种叫作“查询语法”的写法。
如下代码所示:
1 | var items2 = from e in list |
C#编译器会把“查询语法”编译成“方法语法”形式,也就是在运行时它们没有区别。所有的“查询语法”都能用“方法语法”改写,所有的“方法语法”也能用“查询语法”改写。
“查询语法”看起来更新颖,而且比“方法语法”需要写的代码会少一些,但是在编写复杂的查询条件的时候,用“方法语法”编写的代码会更清晰.目前大部分都是使用”方法语法”。