1、Linq

Linq.net core中提供的一种简化的数据查询的技术。使用Linq,可以实现几行代码就可以完成复杂的数据查询。

Linq不仅可以对普通的.Net集合进行查询,而且在Entiy Framework Core 中也被广泛的使用,所以必须熟练掌握``Linq

一、 Lambda表达式

1、委托复习

Lambda表达式是C#中的语法,Lambda表达式在LINQASP.NET Core等很多场合都用得非常多.

要理解Lambda表达式,先要说一下委托。

委托是一种可以指向方法的类型。

如下面的代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
MyDelegate d1 = SayEnglish;
MyDelegate d2 = SayChinese;
string age1 = d1(18);
string age2 = d2(20);

Console.WriteLine(age1);
Console.WriteLine(age2);
static string SayEnglish(int age)
{
return $"My Age is {age}";
}
static string SayChinese(int age)
{
return $"我的年龄是{age}";
}

delegate string MyDelegate(int n);

在上面的代码中,我们定义了委托MyDelegate,并且制定了所指向的方法的参数类型与返回值类型。参数类型是int,返回数据类型是strting,所以我们可以看到,所指向的方法SayEnglishSayChinese符合这个规定。

由于委托变量dl与d2分别指向了SayEnglishSayChinese两个方法,所以我们可以直接调用委托类型的变量,这时候,执行的就是变量指向的方法。

在.NET中定义了最多可达16个参数的泛型委托Action(无返回值)和Func(有返回值),因此一般我们不需要自定义委托类型,可以直接使用Action或者Func这两个委托类型。

1
2
3
4
5
6
7
static string SayChinese(int age)
{
return $"我的年龄是{age}";
}
Func<int, string> fn1 = SayChinese;
string msg = fn1(18);
Console.WriteLine(msg);

在上面的代码中,定义了参数类型是int,返回值类型为string的委托变量fn1.

指向了SayChinese 这个方法,该方法符合fn1这个委托的要求。

下面通过委托变量fn1完成了对SayChinese这个方法的调用。

当然,委托不仅可以指向普通方法,也可以指向匿名方法,如下所示:

1
2
3
4
5
6
7
Func<int, string> fn1 = delegate (int age)
{
return $"My Age is {age}";
};
string msg = fn1(18);
Console.WriteLine(msg);

上面的委托变量fn1指向了一个有int 参数,并且返回值是string类型的匿名方法。

如果想传递多个参数,如下所示:

1
2
3
4
5
6
Func<int, int, string> fn2 = delegate (int i, int j)
{
return $"两个数的和是{i + j}";
};
string result = fn2(2, 3);
Console.WriteLine(result);

在上面的代码中,委托变量 fn2指向的匿名函数需要两个整型参数。

如果委托变量指向的函数,不需要返回值,可以通过Action来定义委托变量,如下代码所示:

1
2
3
4
5
6
Action<int, int> fn3 = delegate (int num1, int num2)
{
Console.WriteLine($"两个数的和是{num1 + num2}");
};
fn3(3,6);

2、Lambda表达式写法

定义匿名函数也可以通过lambda表达式的语法来完成。

Lambda 表达式”是一个匿名函数,它可以包含表达式和语句。可用于创建委托。

运算符 =>,该运算符读为“goes to”。

格式:(input parameters) => expression

如下代码所示:

1
2
3
4
5
6
Func<int, int, string> fn = (int i, int j) =>
{
return $"两个数的和是{i + j}";
};
string result = fn(3, 6);
Console.WriteLine(result);

在上面的代码中,去掉了delegate关键字,然后添加了=>.用=>作为定义方法体的关键字。

其实,这里也可以将参数的类型省略,如下所示:

1
2
3
4
5
6
Func<int, int, string> fn = ( i,  j) =>
{
return $"两个数的和是{i + j}";
};
string result = fn(3, 6);
Console.WriteLine(result);

上面的代码省略了i,j参数的类型,编译器会根据委托类型推断出参数的类型。

接下来还可以进一步简化这些代码。如果=>之后的方法体中只有一行代码,并且方法有返回值,那么还可以省略方法体的花括号及return关键字,如下代码所示:

1
2
3
4
Func<int, int, string> fn = ( i,  j) => $"两个数的和是{i + j}";

string result = fn(3, 6);
Console.WriteLine(result);

3、Lambda简化规则

匿名方法用Lambda表达式改写还有如下简化的规则:

第一:如果一个方法没有返回值,并且方法体只有一行代码,也可以省略方法体的花括号。

关于这一点与上面的例子类似,只不过这里是没有返回值,所以需要使用Action委托。

如下代码所示:

1
2
Action<int, int> fn = (i, j) => Console.WriteLine($"两个数的和是{i + j}");
fn(3, 5);

第二:

如果一个方法只有一个参数,那么Lambda表达式参数中的圆括号也可以省略.

如下代码所示:

1
2
Func<int, int> fn = i => i + 6;
Console.WriteLine(fn(3));

下面我们再来看一个例子,体会一下,Lambda表达式的应用。

该案例的要求:定义一个方法,实现将数组中大于10的数据过滤出来。

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

int[] arrs = { 10,23,1,2,56,12,55,6};
static List<int> MyFilter(int[] s, Func<int,bool> filter)
{
List<int>list=new List<int>();
foreach(int i in s)
{
if (filter(i)) // 调用委托变量指向的lambda
{
list.Add(i);
}
}
return list;
}
List<int> results=MyFilter(arrs, num => num > 10);
foreach(int i in results)
{
Console.WriteLine(i);
}

在上面的代码中,调用MyFilter方法的时候,第二个参数传递的就是一个lambda表达式,如果这里换成匿名函数,会变成如下的形式,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int[] arrs = { 10,23,1,2,56,12,55,6};
static List<int> MyFilter(int[] s, Func<int,bool> filter)
{
List<int>list=new List<int>();
foreach(int i in s)
{
if (filter(i))
{
list.Add(i);
}
}
return list;
}
// 定义fn委托,指向匿名函数
Func<int, bool> fn = delegate (int num)
{
return num > 10;
};
List<int> results=MyFilter(arrs, fn); // 传递匿名函数。
foreach(int i in results)
{
Console.WriteLine(i);
}

通过上面的代码,我们发现匿名函数的写法比较复杂,通过Lambda表达式的写法更加的简单。

4、隐式类型var

在上面的代码中,我们知道MyFilter方法的返回类型是泛型List<int>,所以定义的接收变量results的类型就是List<int>.

写法上稍微复杂,这里完全可以通过var来定义results变量。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int[] arrs = { 10,23,1,2,56,12,55,6};
static List<int> MyFilter(int[] s, Func<int,bool> filter)
{
List<int>list=new List<int>();
foreach(int i in s)
{
if (filter(i))
{
list.Add(i);
}
}
return list;
}
Func<int, bool> fn = delegate (int num)
{
return num > 10;
};
var results=MyFilter(arrs, fn); // 这里指定了var
foreach(int i in results)
{
Console.WriteLine(i);
}

var 关键字会指示编译器根据初始化语句右侧的表达式推断变量的类型。

1
2
3
var i = 6;
var s = "hello";
Console.WriteLine(i+s);

通过ILSpy反编译(对应的dll文件)上面的代码后 ,得到的结果如下所示:

1
2
3
4
5
6
7
8
9
10
internal class Program
{
private static void <Main>$(string[] args)
{
int i = 6;
string s = "hello";
Console.WriteLine(i + s);
}
}

通过反编译的结果可以看到,编译器在编译的时候,确定了变量的类型。

5、匿名类

隐式类型var在匿名类中使用的最多。

将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。 类型名由编译器生成。 每个属性的类型由编译器推断。var

用来初始化属性的表达式不能为 null、匿名函数

1
2
var person = new { name = "zhangsan", age = 18 };
Console.WriteLine(person.name+person.age);

6、C#扩展方法

什么是扩展方法?

简单的理解:扩展方法指的就是向现有的类型中添加新的方法。

扩展方法定义的规则?

(1) 扩展方法所在的类必须声明为static

(2) 扩展方法必须声明为public和static

(3) 扩展方法的第一个参数必须包含关键字this,并且在后面指定扩展的类的名称。

1
2
3
4
5
6
7
8
9
10
Console.WriteLine("3".StringToInt() + 3);// 这里相当于将字符串3转成了整型3
public static class ExpandMethod
{
public static int StringToInt(this string str)
{
return Convert.ToInt32(str);

}
}

通过上面的代码, 我们可以看到ExpandMethod类是一个静态类,同时StringToInt方法是一个publicstatic的方法同时该方法第一个参数必须添加this关键字,同时指定了扩展的类型是string

这样,在字符串中可以直接调用StringToInt方法,而StringToInt方法在原有的string类型中是没有的,现在为其扩展了该方法。

当然,这里,如果你想为扩展方法StringToInt提供额外的参数也是可以的,如下代码所示:

1
2
3
4
5
6
7
8
9
10
Console.WriteLine("3".StringToInt(6) + 3); // 在调用StringToInt方法的时候,传递了6这个参数,最终累加的结果是12
public static class ExpandMethod
{
public static int StringToInt(this string str,int num) // 这里为StringToInt方法,又添加了num参数
{
return Convert.ToInt32(str)+num; // 这里累加了num这参数

}
}

Linq中提供了很多关于集合类的扩展方法。而所有实现了IEnumerable<T>这个接口的类都可以使用这些方法。

这些方法不是IEnumerable<T>中的方法,而是以扩展方法的形式存在于System.Linq命名空间的静态类中。

接下来我们开始讲解LINQ中常用的集合类的扩展方法.

6.1 数据过滤

Linq中提供了where这个扩展方法用于根据条件对数据进行过滤。下面看一下该where方法的使用,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
List<Person> list = new List<Person>()
{
new Person() { Id = 1,Name="zhangsan",Age=19,Salary=2000,Gender=true},
new Person() { Id = 2,Name="老王",Age=29,Salary=20000,Gender=true},
new Person() { Id = 3,Name="laoli",Age=30,Gender=true,Salary=3000}
};
IEnumerable<Person>lists= list.Where(p => p.Salary > 3000 && p.Age < 30);
foreach (Person p in lists)
{
Console.WriteLine(p.Name);
}

class Person
{
public int Id { get; set; }
public string? Name { get; set; }
public int Age { get; set; }
public bool Gender { get; set; }
public double Salary { get; set; }
}

在上面的代码中,定义了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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
List<Person> list = new List<Person>()
{
new Person() { Id = 1,Name="zhangsan",Age=19,Salary=2000,Gender=true},
new Person() { Id = 2,Name="老王",Age=29,Salary=20000,Gender=true},
new Person() { Id = 3,Name="laoli",Age=30,Gender=true,Salary=3000}
};
List<Person> newList = new List<Person>();
foreach (Person person in list)
{
if (person.Salary > 3000 && person.Age < 30)
{
newList.Add(person);
}
}
foreach (Person person in newList)
{
Console.WriteLine(person.Name);
}

通过对比发现,使用Where这个扩展方法在进行数据过滤的时候更加的简单。

6.2 获取数据条数

获取数据的条数可以通过Count这个扩展方法来完成,如下代码所示:

1
2
3
4
5
6
7
8
List<Person> list = new List<Person>()
{
new Person() { Id = 1,Name="zhangsan",Age=19,Salary=2000,Gender=true},
new Person() { Id = 2,Name="老王",Age=29,Salary=20000,Gender=true},
new Person() { Id = 3,Name="laoli",Age=30,Gender=true,Salary=3000}
};

Console.WriteLine(list.Count());// 统计list集合中数据总数。

问题:如果统计薪资大于2000的用户有多少,应该怎样进行过滤呢?

这里需要使用到Count这个扩展方法的另外一种重载的方式。

1
2
3
4
5
6
7
8
9
List<Person> list = new List<Person>()
{
new Person() { Id = 1,Name="zhangsan",Age=19,Salary=2000,Gender=true},
new Person() { Id = 2,Name="老王",Age=29,Salary=20000,Gender=true},
new Person() { Id = 3,Name="laoli",Age=30,Gender=true,Salary=3000}
};


Console.WriteLine(list.Count(p=>p.Salary>2000)); // 可以在Count这个方法中指定过滤条件

当然,有同学可能会想到如下的方式,如下代码所示:

1
2
3
4
5
6
7
8
List<Person> list = new List<Person>()
{
new Person() { Id = 1,Name="zhangsan",Age=19,Salary=2000,Gender=true},
new Person() { Id = 2,Name="老王",Age=29,Salary=20000,Gender=true},
new Person() { Id = 3,Name="laoli",Age=30,Gender=true,Salary=3000}
};
int count=list.Where(p=>p.Salary>2000).Count();
Console.WriteLine(count);

在上面的代码中,先通过Where进行过滤,然后在调用Count方法进行统计,最终的执行结果是一样的。

这里为什么调用了Where方法以后,有可以调用Count方法呢?

我们知道Where方法返回的值类型是IEnumerable类型,所以这里可以继续链式的调用其他的扩展方法。

注意:如果过滤条件返回的数据太多,超出了int类型的最大值,可以调用LongCount方法,该方法返回值的类型是long类型。

LongCount的用法和Count的用法是一样的。

1
2
long count=list.Where(p=>p.Salary>2000).LongCount();
Console.WriteLine(count);

6.3 Any方法使用

Any方法可以用来判断集合中是否至少有一条满足条件的数据,返回值类型为bool.

如下代码所示:

1
2
3
bool result = list.Any(p => p.Salary > 5000);
Console.WriteLine(result);// 返回值是true

运行上面的代码得到的结果是true.

因为,在list这个集合中有一条工资大于5000的记录。

如果,将集合修改成如下的形式:

1
2
3
4
5
6
7
8
9
List<Person> list = new List<Person>()
{
new Person() { Id = 1,Name="zhangsan",Age=19,Salary=2000,Gender=true},
new Person() { Id = 2,Name="老王",Age=29,Salary=20000,Gender=true},
new Person() { Id = 3,Name="laoli",Age=30,Gender=true,Salary=3000},
new Person() { Id = 3,Name="maliu",Age=30,Gender=true,Salary=30000} //这里添加了一个用户,工资大于5000
};
bool result = list.Any(p => p.Salary > 5000);
Console.WriteLine(result);

在上面的list这个集合中,添加了一条工资大于5000的记录,然后运行程序,得到的结果也是true.

这里我们可以得到一个关于Any方法的结论:Any方法只关心“有没有符合条件的数据”,而不关心符合条件的有几条数据,只要有符合条件的数据,返回的结果就是true.

当然,这里我们也可以先执行Where方法进行过滤,然后在调用Any方法,如下所示:

1
2
bool result = list.Where(p => p.Salary > 5000).Any();
Console.WriteLine(result);

当然,有同学可能想到上面的需求也可以通过Count方法来实现,如下所示:

1
2
bool result = list.Count(c => c.Salary > 5000) > 0;
Console.WriteLine(result);

这里首先是通过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
2
3
4
5
6
7
Single:如果确认有且只有一条满足要求的数据,那么就用Single方法。如果没有满足条件的数据,或者满足条件的数据多于一条,Single方法就会抛出异常

SingleOrDefault:如果确认最多只有一条满足要求的数据,那么就用SingleOrDefault方法。如果没有满足条件的数据,SingleOrDefault方法就会返回类型的默认值。如果满足条件的数据多于一条,SingleOrDefault方法就会抛出异常

First:如果满足条件的数据有一条或者多条,First方法就会返回第一条数据;如果没有满足条件的数据,First方法就会抛出异常.

FirstOrDefault:如果满足条件的数据有一条或者多条,FirstOrDefault方法就会返回第一条数据;如果没有满足条件的数据,FirstOrDefault方法就会返回类型的默认值。

具体的代码示例如下所示:

下面先看Single方法的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
List<Person> list = new List<Person>()
{
new Person() { Id = 1,Name="zhangsan",Age=19,Salary=2000,Gender=true},
new Person() { Id = 2,Name="老王",Age=29,Salary=20000,Gender=true},
new Person() { Id = 3,Name="laoli",Age=30,Gender=true,Salary=3000},
new Person() { Id = 3,Name="maliu",Age=30,Gender=true,Salary=30000}
};
Person p = list.Single(p=>p.Id==1);
Console.WriteLine(p.Name); // 返回值是:zhangsan

Person p = list.Single(p=>p.Id==10);
Console.WriteLine(p.Name);// 抛出异常,没有编号为10的记录。

Person p = list.Single(p=>p.Id==3);
Console.WriteLine(p.Name); // 抛出异常,编号为3的记录有2条

下面是关于SingleOrDefault方法的使用,如下所示:

1
2
3
4
5
6
7
8
9
10
11
// SingleOrDefault:如果确认最多只有一条满足要求的数据,那么就用SingleOrDefault方法。如果没有满足条件的数据,SingleOrDefault方法就会返回类型的默认值。如果满足条件的数据多于一条,SingleOrDefault方法就会抛出异常
Person? p = list.SingleOrDefault(p=>p.Id==1); // 这里的SingleOrDefault方法有可能返回null所以,这里需要添加?,表示p是可空类型,所以下面需要进行判断是否为null
if (p == null)
{
Console.WriteLine("没有找到编号为1的记录");
}
else
{
Console.WriteLine(p.Name);
}

修改成以下的过滤条件:

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
2
 Person p= list.First();
Console.WriteLine(p.Name);

这里直接调用了First这个扩展方法,这时候返回的就是List集合中的第一条数据。

当然,也可以给First方法传递过滤的条件,如下代码所示:

1
2
 Person p= list.First(p=>p.Age>20);
Console.WriteLine(p.Name);

我们知道,List集合中存储的用户信息中,年龄大于20的有多条记录,而这时候First方法返回的只是满足条件的第一条记录。

1
2
 Person p= list.First(p=>p.Age>60); // 这行代码会抛出异常。
Console.WriteLine(p.Name);

List集合中,没有年龄大于60,所以执行以上代码会抛出异常。

FirstOrDefault方法的使用

FirstOrDefault:如果满足条件的数据有一条或者多条,FirstOrDefault方法就会返回第一条数据;如果没有满足条件的数据,FirstOrDefault方法就会返回类型的默认值。

1
2
 Person? p= list.FirstOrDefault();
Console.WriteLine(p.Name);

在使用FirstOrDefault方法的时候,如果没有指定过滤条件,默认会返回List集合中第一条记录。

1
2
 Person? p= list.FirstOrDefault(p=>p.Age>20);
Console.WriteLine(p.Name);

我们知道,List集合中存储的用户信息中,年龄大于20的有多条记录,而这时候FirstOrDefault方法返回的只是满足条件的第一条记录。

1
2
3
4
5
6
7
8
9
10
 Person? p= list.FirstOrDefault(p=>p.Age>60);
if (p == null)
{
Console.WriteLine("没有找到");
}
else
{
Console.WriteLine(p.Name);
}

给以上给FirstOrDefault方法指定的条件,找不到满足条件的记录,这时候返回的就是null,所以这里需要进行判断。

6.5 排序

OrderBy方法可以对数据进行正向排序,而OrderByDescending方法则可以对数据进行逆向(倒序)排序

1
2
3
4
5
6
Console.WriteLine("按照年龄正序排序");
IEnumerable<Person>items =list.OrderBy(p => p.Age);
foreach(Person item in items)
{
Console.WriteLine(item.Name);
}

以上代码是按照年龄正序排序

这里的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集合中,用户laolimaliu的年龄是一样的,那这时候会按照什么规则进行排序呢?

这里会根据数据在List集合中的顺序来决定。可以调整以上两个用户在list集合的顺序来看一下过滤的结果。

这里有一个要求:先按照年龄升序排序,如果年龄相同,再按照工资降序排序。

1
2
3
4
5
6
IOrderedEnumerable<Person> items =list.OrderBy(p => p.Age).ThenByDescending(a=>a.Salary);
foreach(Person item in items)
{
Console.WriteLine(item.Name);
}

注意:这里使用了ThenByDescending

问题:先按照年龄降序排序,如果年龄相同,再按照工资升序排序

1
2
3
4
5
6

IOrderedEnumerable<Person> items =list.OrderByDescending(p => p.Age).ThenBy(a=>a.Salary);
foreach(Person item in items)
{
Console.WriteLine(item.Name);
}

注意:这里使用了ThenBy方法。

同时这里还需要注意的一点:千万不要写成:list.OrderBy(p=>p.Age).OrderByDescending(p=>p.Salary)

在以上的排序案例中,我们是针对List这个集合中Person数据进行排序的,但是如果要对数组中的数据进行排序应该怎样处理呢?

1
2
3
4
5
6
7
int[] nums = { 3,5,1,23,67,21,10};
IEnumerable<int> results = nums.OrderBy(n => n);//这里就是根据自己进行排序
foreach (int result in results)
{
Console.WriteLine(result);
}

这里,我们可以总结出一点:针对OrderByOrderByDescending方法进行排序的时候,指定的排序条件不一定是对象中的某个属性,例如以上案例所示,所以说具体的排序条件需要根据具体的情况写任意的表达式。

下面的案例,是根据用户名中最后一个字母进行从大到小的排序,也就是降序排序。

1
2
3
4
5
IOrderedEnumerable<Person> items = list.OrderByDescending(p => p.Name![p.Name!.Length - 1]);
foreach (Person item in items)
{
Console.WriteLine(item.Name);
}

这里通过 p.Name![p.Name!.Length - 1]获取的就是Name这个属性中的最后一个字符。

!表示断言,也就是这里不肯能会出现null.

6.6 限制结果集

所谓的限制结果集,指的就是从集合中获取部分数据。其主要的应用场景就是分页查询。例如:从第2条开始获取3条数据。

这里主要有两个方法。

Skip(n)方法用于跳过n条数据.

Take(n)方法用于获取n条数据。

下面先来看一下Skip()方法的使用

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<Person> list = new List<Person>()
{
new Person() { Id = 1,Name="zhangsan",Age=19,Salary=2000,Gender=true},
new Person() { Id = 2,Name="laowang",Age=29,Salary=20000,Gender=true},

new Person() { Id = 3,Name="laoli",Age=30,Gender=true,Salary=3000},
new Person() { Id = 3,Name="maliu",Age=30,Gender=true,Salary=30000},


};
var items = list.Skip(1);
foreach (var item in items)
{
Console.WriteLine(item.Name);
}

在上面的代码中,使用了Skip(1),表示跳过第一条记录,最终获取到的是剩余的所有的记录。

下面再来看一下Take()方法的使用。

1
2
3
4
5
6
var items = list.Take(1);
foreach (var item in items)
{
Console.WriteLine(item.Name);
}

这里使用了Take(1)表示的就是去取第一条,这里没有进行跳转。

如果想跳过第1条,取2条,应该怎样处理呢?这里可以将Take方法与Skip方法联合来使用。

如下代码所示:

1
2
3
4
5
var items = list.Skip(1).Take(2);
foreach (var item in items)
{
Console.WriteLine(item.Name);
}

问题:先过滤出年龄大于等于30的用户,然后在按照年龄进行升序排序,排序完成后,跳过第1条,取2条记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<Person> list = new List<Person>()
{
new Person() { Id = 1,Name="zhangsan",Age=19,Salary=2000,Gender=true},
new Person() { Id = 2,Name="laowang",Age=29,Salary=20000,Gender=true},

new Person() { Id = 3,Name="laoli",Age=30,Gender=true,Salary=3000},
new Person() { Id = 3,Name="maliu",Age=31,Gender=true,Salary=30000},
new Person() { Id = 3,Name="lisi",Age=32,Gender=true,Salary=31000},

};
var items = list.Where(p=>p.Age>=30).OrderBy(p=>p.Age).Skip(1).Take(2);
foreach (var item in items)
{
Console.WriteLine(item.Name);
}

通过以上的代码可以体会出链式调用。

6.7 聚合函数

我们知道,SQL中有Max(最大值)、Min(最小值)、Avg(平均值)、Sum(总和)、Count(总数)等聚合函数。LINQ中也有对应的方法,它们的名字分别是Max、Min、Average、Sum和Count,这些方法也可以和Where、Skip、Take等方法一起使用,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
List<Person> list = new List<Person>()
{
new Person() { Id = 1,Name="zhangsan",Age=19,Salary=2000,Gender=true},
new Person() { Id = 2,Name="laowang",Age=29,Salary=20000,Gender=true},

new Person() { Id = 3,Name="laoli",Age=30,Gender=true,Salary=3000},
new Person() { Id = 3,Name="maliu",Age=31,Gender=true,Salary=30000},
new Person() { Id = 3,Name="lisi",Age=32,Gender=true,Salary=31000},

};
int maxAge = list.Max(p => p.Age);
Console.WriteLine("最大年龄是:" + maxAge);
int minAge=list.Min(p => p.Age);
Console.WriteLine("最小的年龄是:" + minAge);
double avgSalary = list.Average(p => p.Salary);
Console.WriteLine("平均工资是:" + avgSalary);
double sumSalary=list.Sum(p => p.Salary);
Console.WriteLine("工资综合是:"+sumSalary);
int count=list.Count();
Console.WriteLine("总的记录数是:" + count);

在前面我们也强调过,这些聚合函数可以和其他的扩展方法一起使用。

例如,这里有一个需求:统计出年龄大于等于30的用户中,最高工资是多少?

1
2
double maxSalary=list.Where(p => p.Age >= 30).Max(p => p.Salary);
Console.WriteLine("大于等于30岁的用户中最高工资是" + maxSalary);

如果集合是int值类型的集合,我们也可以使用没有参数的聚合函数,如下代码所示:

1
2
3
int[] arr = { 56,12,31,67,89,2,3,55,59,90};
Console.WriteLine("最小的数"+arr.Min());
Console.WriteLine("大于50的数平均值:" + arr .Where(i => i > 50).Average());

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
List<Person> list = new List<Person>()
{
new Person() { Id = 1,Name="zhangsan",Age=19,Salary=2000,Gender=true},
new Person() { Id = 2,Name="laowang",Age=29,Salary=20000,Gender=true},

new Person() { Id = 3,Name="laoli",Age=30,Gender=true,Salary=3000},
new Person() { Id = 3,Name="maliu",Age=30,Gender=true,Salary=30000},// 这里修改了年龄是30
new Person() { Id = 3,Name="lisi",Age=32,Gender=true,Salary=31000},// 这里修改了年龄是30

};
IEnumerable <IGrouping<int,Person>>items= list.GroupBy(c => c.Age);
// 这里获取的是每一个分组
foreach(IGrouping<int,Person>item in items)
{
Console.WriteLine(item.Key); // 这里的key表示分组的依据,这里是根据Age进行分组的,所以key中保存的就是age.
// 这里是打印每一组中的用户。
foreach(Person p in item)
{
Console.WriteLine(p.Name);
}
Console.WriteLine("****************");
}

当然,上面去写类型的时候,比较麻烦,所以这里我们都使用类型推断var.

如下所示:

1
2
3
4
5
6
7
8
9
10
11
var items= list.GroupBy(c => c.Age);
foreach(var item in items)
{
Console.WriteLine(item.Key);
foreach(Person p in item)
{
Console.WriteLine(p.Name);
}
Console.WriteLine("****************");
}

我们知道IGrouping也是一个集合,所以下面我们可以计算出这一组中相关的一些数据,例如,当前每一组有多少条记录,没一组中最大的工资等等。

如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
var items= list.GroupBy(c => c.Age);
foreach(var item in items)
{
Console.WriteLine(item.Key);
Console.WriteLine("当前组的人数:"+item.Count());
Console.WriteLine("当前组最高工资:" + item.Max(c => c.Salary));
Console.WriteLine("当前组平均工资:" + item.Average(c => c.Salary));
foreach(Person p in item)
{
Console.WriteLine(p.Name);
}
Console.WriteLine("****************");
}

注意: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表中的nameage两个字段筛选出来,这就是投影。

1
2
3
4
5
6
IEnumerable<int> ages = list.Select(p => p.Age);
foreach (int age in ages)
{
Console.WriteLine(age);
}

在上面的代码中,Select方法把list中每一项的Age通过投影操作提取出来,因为Ageint类型的,所以Select方法的返回值就是IEnumerable<int>类型.

下面筛选出用户的姓名

1
2
3
4
5
IEnumerable<string> names = list.Select(p => p.Name!);
foreach (string n in names)
{
Console.WriteLine(n);
}

如果想将用户名和年龄都筛选出来呢?

1
2
3
4
5
6
IEnumerable<string> names = list.Select(p => p.Name!+","+p.Age);
foreach (string n in names)
{
Console.WriteLine(n);
}

下面,筛选出工资大于3000的用户的性别:

1
2
3
4
5
IEnumerable<string> names = list.Where(p=>p.Salary>3000).Select(p => p.Gender?"男":"女");
foreach (string n in names)
{
Console.WriteLine(n);
}

Select方法中也可以使用匿名类型.

Select方法从list的每一项中提取出NameAge属性的值,并且把Gender转换为字符串,Select方法的返回值是一个匿名类型的IEnumerable类型,因此我们必须用var声明变量类型。

1
2
3
4
5
var items = list.Select(p => new { Name = p.Name, Age = p.Age, Gender = p.Gender ? "男" : "女" });
foreach (var item in items)
{
Console.WriteLine(item.Name+","+item.Age+","+item.Gender);
}

也可以与Group by一起使用。

1
2
3
4
5
var items = list.GroupBy(p => p.Age).Select(c => new { Age = c.Key,MaxSalary=c.Max(a=>a.Salary),count=c.Count() });
foreach (var item in items)
{
Console.WriteLine(item.Age+","+item.MaxSalary+","+item.count);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var items = list.Select(a => new { Age = a.Age, UserName = a.Name }).GroupBy(a => a.Age);
foreach (var item in items)
{
foreach (var i in item)
{
Console.WriteLine(i.UserName);
Console.WriteLine(i.Age);
}
Console.WriteLine("----------------");
}

/*IEnumerable<IGrouping<int, Person>> items = list.GroupBy(a => a.Age);
foreach (IGrouping<int, Person> groupItem in items)
{
groupItem.Select(a => new {Id=a.Id });
}
*/

6.10 链式调用

通过前面的学习,我们知道linq中是可以进行链式调用的。这是因为像Where、Select、OrderBy、GroupBy、Take、Skip等方法的返回值都是IEnumerable<T>类型,因此它们是可以被链式调用的.

下面我们再来看一个例子:

获取Id>2的数据,再按照Age分组,并且把分组按照Age排序,然后取出前2条,最后投影取得年龄、人数、平均工资

1
2
3
4
5
6
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
foreach (var item in items)
{
Console.WriteLine($"年龄是:{item.Age},人数是:{item.count},平均工资:{item.avgSalary}");
}

6.11 LINQ的另一种写法

前面我们学到的使用Where、OrderBy、Select等扩展方法进行数据查询的写法叫作“方法语法”。除此之外,LINQ还有另外一种叫作“查询语法”的写法。

如下代码所示:

1
2
3
4
var items2 = from e in list
where e.Salary > 3000
orderby e.Age
select new { e.Name, e.Age, Gender = e.Gender ? "男" : "女" };

C#编译器会把“查询语法”编译成“方法语法”形式,也就是在运行时它们没有区别。所有的“查询语法”都能用“方法语法”改写,所有的“方法语法”也能用“查询语法”改写。

“查询语法”看起来更新颖,而且比“方法语法”需要写的代码会少一些,但是在编写复杂的查询条件的时候,用“方法语法”编写的代码会更清晰.目前大部分都是使用”方法语法”。