Linq的基本操作及简单例子(重点Group操作)
参考资料:
bilibili杨中科.NET视频教程
MSDN文档
- 先准备一下测试数据:
- Where:
- Count:
- Any:
- Single & SingleOrDefault:
- OrderBy & OrderByDescending & ThenBy & ThenByDescending :
- Skip & Take:
- Sum & Average & Max & Min:
- Group:
- 多条件Group:
- 在例19的group2的基础上进行Where筛选:
- 先分组,然后使用分组的数据进行新数据的构造:
- 综合运用Where,Group,OrderBy,Select:
- 难道分完组之后只能用group的Key进行排序吗?当然不是:
- 一些其他的常用操作:
- 综合运用举例:
学习C#时,发现C#有一个很神奇的Linq,虽然学了一些,但一直对其中的分组操作不太熟悉。今天借杨中科老师的例子中的示例数据来学习一下Group,以及结合Group实现的一些操作。杨老师视频中说Linq大多数场景下效率都很不错,看上去是个值得经常使用的东西。
先准备一下测试数据:
声明一个Employee类:
class Employee
{
public long Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public bool Gender { get; set; }
public int Salary { get; set; }
public override string ToString()
{
return $"Id={Id}, Name={Name}, Age={Age}, Gender={Gender}, Salary={Salary}";
}
}
构造一个list,用来进行一系列操作:
List<Employee> list = new();
list.Add(new Employee() { Id = 1, Name = "jerry", Age = 28, Gender = true, Salary = 5000 });
list.Add(new Employee() { Id = 2, Name = "jim", Age = 33, Gender = true, Salary = 3000 });
list.Add(new Employee() { Id = 3, Name = "lily", Age = 35, Gender = false, Salary = 9000 });
list.Add(new Employee() { Id = 4, Name = "lucy", Age = 16, Gender = false, Salary = 2000 });
list.Add(new Employee() { Id = 5, Name = "kimi", Age = 25, Gender = true, Salary = 1000 });
list.Add(new Employee() { Id = 6, Name = "nancy", Age = 35, Gender = false, Salary = 8000 });
list.Add(new Employee() { Id = 7, Name = "zack", Age = 35, Gender = true, Salary = 8500 });
list.Add(new Employee() { Id = 8, Name = "jack", Age = 33, Gender = true, Salary = 8000 });
先熟悉一下一些基本操作:
Where:
// 1. 获取 Age>30 且 Salary>=5000 的员工
var list1 = list.Where(e => e.Age > 30 && e.Salary >= 5000).ToList();
list1.ForEach(e => Console.WriteLine(e));
// 运行结果
//Id = 3, Name = lily, Age = 35, Gender = False, Salary = 9000
//Id = 6, Name = nancy, Age = 35, Gender = False, Salary = 8000
//Id = 7, Name = zack, Age = 35, Gender = True, Salary = 8500
//Id = 8, Name = jack, Age = 33, Gender = True, Salary = 8000
Count:
// 2. 统计 Age>30 且 Salary>=5000 的员工个数
Console.WriteLine(list.Count(e => e.Age > 30 && e.Salary >= 5000));
// 运行结果:
//4
Any:
// 3. 查询是否有工资大于10000的员工
Console.WriteLine(list.Any(e => e.Salary > 10000));
// 运行结果:
//False
// 4. 查询是否有工资大于5000的员工
Console.WriteLine(list.Any(e => e.Salary > 5000));
// 运行结果:
//True
Single & SingleOrDefault:
// 5. Single
Console.WriteLine(list.Single());
// 运行结果:
//抛异常:System.InvalidOperationException: 'Sequence contains more than one element'
// 6. 有条件的Single
Console.WriteLine(list.Single(e => e.Name == "jerry"));
// 运行结果:
//Id=1, Name=jerry, Age=28, Gender=True, Salary=5000
// 7. SingleOrDefault
Console.WriteLine(list.SingleOrDefault());
// 运行结果:
//抛异常:System.InvalidOperationException: 'Sequence contains more than one element'
// 8. 有条件的SingleOrDefault
Console.WriteLine(list.SingleOrDefault(e => e.Name == "Fuck"));
// 运行结果:
//空白,啥也没有,可能是因为Employee没有写带参数的构造函数,我们没有用构造函数来初始化
OrderBy & OrderByDescending & ThenBy & ThenByDescending :
// 9. 随机排序
var r = new Random();
var list2 = list.OrderBy(e => r.Next()).ToList();
list2.ForEach(e => Console.WriteLine(e));
// 运行结果:
//Id = 8, Name = jack, Age = 33, Gender = True, Salary = 8000
//Id = 2, Name = jim, Age = 33, Gender = True, Salary = 3000
//Id = 7, Name = zack, Age = 35, Gender = True, Salary = 8500
//Id = 1, Name = jerry, Age = 28, Gender = True, Salary = 5000
//Id = 4, Name = lucy, Age = 16, Gender = False, Salary = 2000
//Id = 5, Name = kimi, Age = 25, Gender = True, Salary = 1000
//Id = 6, Name = nancy, Age = 35, Gender = False, Salary = 8000
//Id = 3, Name = lily, Age = 35, Gender = False, Salary = 9000
// 10. 按年龄从小到大排序
var list3 = list.OrderBy(e => e.Age).ToList();
list3.ForEach(e => Console.WriteLine(e));
// 运行结果:
//Id = 4, Name = lucy, Age = 16, Gender = False, Salary = 2000
//Id = 5, Name = kimi, Age = 25, Gender = True, Salary = 1000
//Id = 1, Name = jerry, Age = 28, Gender = True, Salary = 5000
//Id = 2, Name = jim, Age = 33, Gender = True, Salary = 3000
//Id = 8, Name = jack, Age = 33, Gender = True, Salary = 8000
//Id = 3, Name = lily, Age = 35, Gender = False, Salary = 9000
//Id = 6, Name = nancy, Age = 35, Gender = False, Salary = 8000
//Id = 7, Name = zack, Age = 35, Gender = True, Salary = 8500
// 11. 按 Name 的最后一个字母逆序排序
var list4 = list.OrderByDescending(e => e.Name[^1]).ToList(); // e.Name[^1] 相当于 e.Name[e.Name.Length - 1]
list4.ForEach(e => Console.WriteLine(e));
// 运行结果:
//Id = 1, Name = jerry, Age = 28, Gender = True, Salary = 5000
//Id = 3, Name = lily, Age = 35, Gender = False, Salary = 9000
//Id = 4, Name = lucy, Age = 16, Gender = False, Salary = 2000
//Id = 6, Name = nancy, Age = 35, Gender = False, Salary = 8000
//Id = 2, Name = jim, Age = 33, Gender = True, Salary = 3000
//Id = 7, Name = zack, Age = 35, Gender = True, Salary = 8500
//Id = 8, Name = jack, Age = 33, Gender = True, Salary = 8000
//Id = 5, Name = kimi, Age = 25, Gender = True, Salary = 1000
// 12. 先按年龄排序,再按工资逆序排
var list5 = list.OrderBy(e => e.Age).ThenByDescending(e => e.Salary).ToList();
list5.ForEach(e => Console.WriteLine(e));
// 运行结果:
//Id = 4, Name = lucy, Age = 16, Gender = False, Salary = 2000
//Id = 5, Name = kimi, Age = 25, Gender = True, Salary = 1000
//Id = 1, Name = jerry, Age = 28, Gender = True, Salary = 5000
//Id = 8, Name = jack, Age = 33, Gender = True, Salary = 8000
//Id = 2, Name = jim, Age = 33, Gender = True, Salary = 3000
//Id = 3, Name = lily, Age = 35, Gender = False, Salary = 9000
//Id = 7, Name = zack, Age = 35, Gender = True, Salary = 8500
//Id = 6, Name = nancy, Age = 35, Gender = False, Salary = 8000
Skip & Take:
// 13. 跳过前两个,再取三个
var list6 = list.Skip(2).Take(3).ToList();
list6.ForEach(e => Console.WriteLine(e));
// 运行结果:
//Id = 3, Name = lily, Age = 35, Gender = False, Salary = 9000
//Id = 4, Name = lucy, Age = 16, Gender = False, Salary = 2000
//Id = 5, Name = kimi, Age = 25, Gender = True, Salary = 1000
Sum & Average & Max & Min:
// 14. 统计30岁以上员工的工资总数(资本家扶了一下眼镜🧐)
var sumSalary = list.Where(e => e.Age > 30).Sum(e => e.Salary);
Console.WriteLine(sumSalary);
// 运行结果:36500
// 15. 统计30岁以上员工的工资平均数
var avgSalary = list.Where(e => e.Age > 30).Average(e => e.Salary);
Console.WriteLine(avgSalary);
// 运行结果:7300
// 16. 取工资最高的人的工资
var maxSalary = list.Max(e => e.Salary);
Console.WriteLine(maxSalary);
// 运行结果:9000
// 17. 取年龄最小的人的年龄
var minAge = list.Min(e => e.Age);
Console.WriteLine(minAge);
// 运行结果:16
Group:
// 18. 按年龄分组,然后遍历分组
var group1 = list.GroupBy(e => e.Age).ToList();
group1.ForEach(g =>
{
Console.WriteLine("Key: " + g.Key);
Console.WriteLine("最大工资:" + g.Max(e => e.Salary));
foreach (Employee e in g)
{
Console.WriteLine(e);
}
});
// 运行结果:
//Key: 28
//最大工资: 5000
//Id = 1, Name = jerry, Age = 28, Gender = True, Salary = 5000
//Key: 33
//最大工资: 8000
//Id = 2, Name = jim, Age = 33, Gender = True, Salary = 3000
//Id = 8, Name = jack, Age = 33, Gender = True, Salary = 8000
//Key: 35
//最大工资: 9000
//Id = 3, Name = lily, Age = 35, Gender = False, Salary = 9000
//Id = 6, Name = nancy, Age = 35, Gender = False, Salary = 8000
//Id = 7, Name = zack, Age = 35, Gender = True, Salary = 8500
//Key: 16
//最大工资: 2000
//Id = 4, Name = lucy, Age = 16, Gender = False, Salary = 2000
//Key: 25
//最大工资: 1000
//Id = 5, Name = kimi, Age = 25, Gender = True, Salary = 1000
该例子中,分组并且 ToList()
后的 group1 的类型为List<IGrouping<int, Employee>>
:
如果不 ToList()
,类型应该为 IEnumerable<IGrouping<int, Employee>>
。int即为被分好的值几个group的键的类型,Employee为该group的值的类型。
可以先对IEnumerable<IGrouping<int, Employee>>
进行foreach,得到的是一个个IGrouping<int, Employee>
,然后直接对 IGrouping<int, Employee>
进行foreach
操作,得到的都是这个组里的Employee。
多条件Group:
// 19. 多条件分组,按照 Age 和 Gender 分组
var group2 = list.GroupBy(e => new { e.Age, e.Gender });
foreach (var g in group2)
{
Console.WriteLine("Key: " + g.Key);
Console.WriteLine("最大工资:" + g.Max(e => e.Salary));
foreach (Employee e in g)
{
Console.WriteLine(e);
}
Console.WriteLine();
}
// 运行结果:
//Key: { Age = 28, Gender = True }
//最大工资: 5000
//Id = 1, Name = jerry, Age = 28, Gender = True, Salary = 5000
//Key: { Age = 33, Gender = True }
//最大工资: 8000
//Id = 2, Name = jim, Age = 33, Gender = True, Salary = 3000
//Id = 8, Name = jack, Age = 33, Gender = True, Salary = 8000
//Key: { Age = 35, Gender = False }
//最大工资: 9000
//Id = 3, Name = lily, Age = 35, Gender = False, Salary = 9000
//Id = 6, Name = nancy, Age = 35, Gender = False, Salary = 800
//Key: { Age = 16, Gender = False }
//最大工资: 2000
//Id = 4, Name = lucy, Age = 16, Gender = False, Salary = 2000
//Key: { Age = 25, Gender = True }
//最大工资: 1000
//Id = 5, Name = kimi, Age = 25, Gender = True, Salary = 1000
//Key: { Age = 35, Gender = True }
//最大工资: 8500
//Id = 7, Name = zack, Age = 35, Gender = True, Salary = 8500
这里使用了匿名类来进行多条件筛选,可以看到分好组的group2的类型为IEnumerable<IGrouping<'a, Employee>>
,即键为'a
,而且下面也说明了匿名类型'a
是new { int Age, bool Gender }
:
在例19的group2的基础上进行Where筛选:
// 20. 在例19的group2的基础上进行筛选,筛选Age为35,且Gender为false的员工
var group3 = group2.Where(g => g.Key.Age == 35 && g.Key.Gender == false);
foreach (var g in group3)
{
foreach (Employee e in g)
{
Console.WriteLine(e);
}
Console.WriteLine();
}
// 运行结果:
//Id = 3, Name = lily, Age = 35, Gender = False, Salary = 9000
//Id = 6, Name = nancy, Age = 35, Gender = False, Salary = 8000
group3的类型和group2是相同的:
可以看到上面代码中group2.Where
中可以直接g.Key.Age
,因为g.Key
是一个匿名类。而且用g.Key
只能取到匿名类中的属性,即Age和Gender。不作为GroupBy的参数的其他属性,如Id和Name,都取不到。这一点跟SQL中的GROUP BY
操作非常相似。我之前一直不理解为什么SQL中的那种规定,现在竟然借助Linq理解了一些SQL。
同样,上面代码中foreach
一个IGrouping<'a, Employee>
,也可以取到每一个分组的每一个Employee。
先分组,然后使用分组的数据进行新数据的构造:
// 21. 先按照Age分组,再取得每个分组的最大工资,最小工资,平均工资,以及分组内的人数
var group3 = list.GroupBy(e => e.Age)
.Select(g => new
{
Age = g.Key,
MaxSalary = g.Max(e => e.Salary),
MinSalary = g.Min(e => e.Salary),
AvgSalary = g.Average(e => e.Salary),
Count = g.Count()
});
foreach (var item in group3)
{
Console.WriteLine(item.Age + "," + item.MaxSalary + "," + item.MinSalary + "," + item.AvgSalary + "," + item.Count);
}
// 运行结果:
//28,5000,5000,5000,1
//33,8000,3000,5500,2
//35,9000,8000,8500,3
//16,2000,2000,2000,1
//25,1000,1000,1000,1
综合运用Where,Group,OrderBy,Select:
// 22. 先筛选年龄不小于20岁的,然后按照年龄分组,分好组之后,各个组之间根据Age排序,即根据组的Key排序,然后选前3个组,构造一个包含年龄,数量,平均工资的数据结构
var list3 = list.Where(e => e.Age >= 20)
.GroupBy(e => e.Age)
.OrderBy(g => g.Key)
.Take(3)
.Select(g => new { Age = g.Key, Count = g.Count(), AvgSalary = g.Average(e => e.Salary) });
foreach (var item in list3)
{
Console.WriteLine(item);
}
// 运行结果:
//{ Age = 25, Count = 1, AvgSalary = 1000 }
//{ Age = 28, Count = 1, AvgSalary = 5000 }
//{ Age = 33, Count = 2, AvgSalary = 5500 }
难道分完组之后只能用group的Key进行排序吗?当然不是:
// 23. 与例22条件相似,不过这次先按照Age分组,按照Age排序,再在组内按照工资正序排序
var list4 = list.Where(e => e.Age >= 20)
.GroupBy(e => e.Age)
.OrderBy(g => g.Key)
.Select(g => g.OrderBy(e => e.Salary));
foreach (var l in list4)
{
foreach (var item in l)
{
Console.WriteLine(item);
}
Console.WriteLine();
}
// 运行结果:
//Id = 5, Name = kimi, Age = 25, Gender = True, Salary = 1000
//Id = 1, Name = jerry, Age = 28, Gender = True, Salary = 5000
//Id = 2, Name = jim, Age = 33, Gender = True, Salary = 3000
//Id = 8, Name = jack, Age = 33, Gender = True, Salary = 8000
//Id = 6, Name = nancy, Age = 35, Gender = False, Salary = 8000
//Id = 7, Name = zack, Age = 35, Gender = True, Salary = 8500
//Id = 3, Name = lily, Age = 35, Gender = False, Salary = 9000
同时注意一波这里list4的类型:
一些其他的常用操作:
// 24. 有一个由整数(逗号分隔)构成的字符串,取该字符串中各个整数的平均值:
var intArrStr = "53,33,55,888,9";
var avg = intArrStr.Split(",")
.Select(s => int.Parse(s.Trim()))
.Average();
Console.WriteLine(avg);
// 运行结果:207.6
综合运用举例:
// 25. 将一个大小写英文字母,数字,符号混合组成的字符串,统计其中字母出现的频率,取出现两次以上的,并根据频率从大到小排序
var str = "hFfm3hsS34xgd2rxa1@32Ae%hV$$Ci^v5*yW(neur53x)HKmy@eh!fx5#i4HdySgFfzyudgfvi86sdMzlzh";
var freStat = str.Where(c => char.IsLetter(c))
.Select(c => char.ToLower(c))
.GroupBy(l => l)
.OrderByDescending(g => g.Count())
.Where(g => g.Count() > 2)
.Select(g => new { Letter = g.Key, Frequency = g.Count() });
foreach (var item in freStat)
{
Console.WriteLine($"letter:{item.Letter}, frequency:{item.Frequency}");
}
// 运行结果:
//letter: h, frequency: 7
//letter: f, frequency: 6
//letter: s, frequency: 4
//letter: x, frequency: 4
//letter: d, frequency: 4
//letter: y, frequency: 4
//letter: m, frequency: 3
//letter: g, frequency: 3
//letter: e, frequency: 3
//letter: v, frequency: 3
//letter: i, frequency: 3
//letter: z, frequency: 3