集合的增删改查中,最有技术含量的就是:查。
在过去很长一段时间里,我们只能在for/foreach循环中进行遍历和筛选,直到出现了
Language-Integrated Query,集成查询语言。
所谓“集成”,指Linq并非只针对集合,它已经作用于数据库(Linq to SQL/EF)、XML文件(Linq to XML)、Web Service……针对于集合的操作属于Linq to Object。但所有的Linq使用统一的查询表达式(query expression)。
PS:个人感觉,从Linq开始,C#永远的将Java甩在身后!Java(以及其他语言)没有Linq,勉强对标Linq的是stream。
演示用数据:
Teacher fg = new Teacher { Name = "大飞哥", Age = 41 }; Teacher fish = new Teacher { Name = "小鱼", Age = 28 }; Teacher waiting = new Teacher { Name = "诚聘" }; IEnumerable<Teacher> teachers = new List<Teacher> { fg, fish, waiting }; Major csharp = new Major { Name = "C#", Teacher = fg }; Major SQL = new Major { Name = "SQL", Teacher = fg }; Major Javascript = new Major { Name = "Javascript", Teacher = fg }; Major UI = new Major { Name = "UI", Teacher = fish }; IEnumerable<Major> majors = new List<Major> { csharp, SQL, Javascript, UI }; IList<Student> students = new List<Student> { new Student{Score = 98, Name = "屿", Majors=new List<Major>{csharp,SQL } }, new Student{Score = 86, Name = "行人", Majors=new List<Major>{Javascript, csharp, SQL} }, new Student{Score = 78, Name = "王平", Majors=new List<Major>{csharp}}, new Student{Score = 89, Name = "王枫", Majors=new List<Major>{Javascript, csharp, SQL,UI}}, new Student{Score = 98, Name = "蒋宜蒙", Majors=new List<Major>{Javascript, csharp}}, };
先睹为快,条件过滤,取出所有成绩(Score)大于90的学生(Student):
var excellent = students.Where(s => s.Score > 90);
Where()方法的:
Where()是名称空间System.Linq下的一个扩展方法(复习)
public static IEnumerable<TSource> Where<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate)
可以由任何IEnumerable<TSource>对象(比如List<Student>)调用。(#体会#:泛型和继承/多态的作用)
Func<TSource, bool> predicate,代表的是过滤/筛选条件:
基于TSource(集合元素)进行运算,返回一个bool值,确定是否满足要求。
任何一个能返回bool值的、传入参数为TSource的lambda表达式都可以用作where条件(尤其是在Linq to Object中):
s => s.Name.StartsWith("王") s => s.Name.StartsWith("王") && s.Score > 80 //甚至实际上不用s s => true
PS:不要因为Lambda的参数是Student类型,不是int等简单类型就懵了
IEnumerable<TSource>,(惯例)Linq运算的结果可以用var声明,因为以后这种类型可能会变得越来越臃肿复杂。
然后,使用foreach循环输出:
foreach (var item in excellents) { Console.WriteLine(item.Name);
Linq(to Object)的本质还是foreach。复习:面向函数中的filter函数
为了加深对上述Linq方法的理解(兼复习),我们自己来写一个Where条件的方法。
static class Mimic { internal static IEnumerable<T> MimicWhere<T>( this IEnumerable<T> source, Func<T, bool> predicate) { foreach (var item in source) { if (predicate(item)) { yield return item; } } }
注意:使用yield避免额外的集合声明
对集合中元素进行排序。需指定排序依据:
students.OrderBy(s=>s.Score);
students.OrderByDescending(s=>s.Name);
注意:尽量直接使用OrderBy()或OrderByDescending(),不要在OrderBy()之后再Reverse(),养成习惯,尤其是在后面Linq to SQL/EF时,注意会造成性能浪费。(暂时理解成:先排序再颠倒不如一次性排序)
排序是依赖于比较的,int、string等都是天然可比较的(实现了IComparable)
演示:用“不可比较”的属性进行排序,会报错:
students.OrderByDescending(s => s.Majors);
因为.NET运行时会懵:两个List我咋比较?
如果我们有确定的比较方案,比如比较他们元素的个数,可以
首先需要自己声明一个实现IComparer的类:
class MajorComparor<T> : IComparer<IList<T>> { public int Compare([AllowNull] IList<T> x, [AllowNull] IList<T> y) { return x.Count() - y.Count();
然后传入其对象:
students.OrderByDescending(s => s.Majors, new MajorComparor<Major>());
实现先按某字段(比如Score)排序,当Score成绩相同时,再按另一个字段(比如Majors)排序的功能:
var excellents = students.OrderBy(s=>s.Score) .ThenBy(s => s.Majors, new MajorComparor<Major>());
不要使用:OrderBy().OrderBy(),这样前面一个OrderBy()会被后面一个OrderBy()覆盖……
又被称之为延迟(deferred)/惰性(lazy)加载,指的是:
Linq的查询方法只是一个表达式,并不存储查询结果!
只有当迫不得已的时候,才真正的进行运算……
#理解#:当OrderBy()用于比较的属性“不可比较”时,直到foreach时才报运行时错误。
Forcing Immediate Execution,指的是foreach以外,能够要求Linq方法立即进行查询运算的方法,常见的有:
ToList()/ToArray()/ToXXX等方法:将IEnumerable转换成List/数组/其他集合对象
List<Student> list = excellents.ToList();
Student[] array = excellents.ToArray();
First()/Single()/Last(),获取第一个/唯一一个/最后一个元素。
演示:如果说
Console.WriteLine(excellents.SingleOrDefault()?.Name);意思是:如果没有符合条件的元素,返回该元素类型的默认值
Sum/Count/Average/Min/Max:求和/元素个数/平均值/最小值/最大值
Console.WriteLine(excellents.Sum(s=>s.Score)); //区分Count属性,Count()方法里还能传过滤条件 Console.WriteLine(excellents.Count(/*s=>s.Name.StartsWith("王")*/)); Console.WriteLine(excellents.Average(s=>s.Score)); Console.WriteLine(excellents.Max(s=>s.Score)); Console.WriteLine(excellents.Min(s=>s.Score));
两个原因:
//假设students(数据源)发生了变化 students.Add(new Student { Score = 65, Name = "" });
Take(m):从集合中“尽可能的”取m个元素。即:如果集合中满足条件的元素个数小于m,就取所有元素。
使用场景:取成绩最高的前三名同学
var excellents = students .OrderByDescending(s => s.Score) .Take(3) ;
Skip(n):跳过n个元素
综合使用场景:分页。比如,每页10条,取第3页的数据就是:跳过(3-1)*10(最多)取10条数据
var excellents = students .OrderByDescending(s => s.Score) .Skip(20) .Take(10) ;
@想一想@:Take()和Skip()是强制立即执行方法么?如何验证?
演示:使用Major进行排序,如果Skip()的数量超过了集合中元素数量,甚至都不报错……有意思!
比如说我们要通过majors统计每个老师上了多少门课,首先就要
分组,把所有的Majors按其授课老师分组:
var groupedMajor = majors.GroupBy(m => m.Teacher);
分组过后得到的结果是什么?IEnumerable<IGrouping<Teacher, Major>>,其关键是:
注意IGrouping实现了IEnumerable,所以可以直接被foreach获取小组里每一个元素。
这可能不太容易理解,飞哥更喜欢的是这种结构:
public interface IGrouping{ TKey Key; IEnumerable<TElement> Items; }你呢?
演示:使用foreach遍历IGrouping中的值:
foreach (var item in groupedMajor) { Console.WriteLine(item.Key.GetType() + ":" + item.Key.Name); foreach (var i in item) { Console.WriteLine(" " + i.GetType() + ":" + i.Name); } Console.WriteLine(); }
有时候我们需要用多个属性进行分组,这时候用匿名对象即可:
majors.GroupBy(m => new { m.Name, m.Age });
更多的时候,我们是利用聚合函数进行统计,得到各个小组的:数量/最大/最小/和/平均值等……
foreach (var item in groupedMajor) { Console.WriteLine(item.Key.Name + ":" + item.Count()); }
但上述结果我们不直接输出,而是将其存储起来,如何操作?
可以用ToDictionary(),将老师(名称)做键,课程数量做值:
Dictionary<string, int> pairs = groupedMajor.ToDictionary(gm => gm.Key.Name, gm => gm.Count());
投影:可以将集合中每个元素的一个或多个属性抽取出来,再组合成一个新集合。
IEnumerable<double> scores = students.Select(s => s.Score);
注意不要混淆:select和where
可以使用
class Score { public string Name { get; set; } public double Value { get; set; } }然后在select的时候:
IEnumerable<Score> scores = students.Select( s => new Score { Name = s.Name, Value = s.Score });
还可以使用
复习:匿名类
但此时只能使用var声明变量:
var scores = students.Select( s => new //没有类名了 { Name = s.Name, Value = s.Score });
更进一步,属性名都可以省略:
var scores = students.Select( s => new { s.Name, s.Score });
这样匿名类会使用“原”属性名,一样的可以foreach:
foreach (var item in scores) { Console.WriteLine(item.Name + ": " + item.Score);
@想一想@:如何获取一个集合,显示每个老师的姓名、年龄和所教授的课程?
var tm = majors.Select(m => new { MajorName = m.Name, TeacherName = m.Teacher.Name, TeacherAge = m.Teacher.Age });
但假设Major中记录的不是课程的老师对象,而是老师的姓名呢?
其实在Linq to Object中并不必要,因为类的关联,总有一些其他办法。但我们还是应该掌握以下用法,以备日后Linq to SQL所用
public class Major { //public Teacher Teacher { get; internal set; } public string Teacher { get; internal set; }
//Major csharp = new Major { Name = "C#", Teacher = fg }; Major csharp = new Major { Name = "C#", Teacher = "大飞哥" };
如何获取上述集合?这就需要用Join(),连接若干个集合:
var tm = majors.Join(teachers, //majors连接teachers m => m.Teacher, t => t.Name, //连接依赖/比较的属性 (m, t) => new //生成的匿名对象作为结果集合元素 { MajorName = m.Name, TeacherName = t.Name, TeacherAge = t.Age });
PS:Join方法不如Linq表达式有表现力,所以此处只简单介绍。
需求:找出所学课程名包含了字母s(不论大小写)的学生,将学生及其对应的课程作为集合元素存储
注意这几个数据:
Major Css = new Major { Name = "CSS" };
#理解#必须同时满足两个条件:
思路就是先把所有学生的所有课程取出来,将之前的Student:List<Major>(1:n)变成Student:Major(1:1),然后再进行过滤:
var result = students.SelectMany( s => s.Majors, //指示取出students里的所有Major (s, m) => new { Student = s, MajorName = m.Name } //组合student和major ) .Where(sm => sm.MajorName.ToLower().Contains("s")) //在上述结果集中筛选 ; foreach (var item in result) { Console.WriteLine($"{item.Student.Name}:{item.MajorName}");
形象理解:将原集合中每个元素中“横向”的转“纵向”
难点/练习:查看方法声明,泛型和Func……
见:J&C:集合概述 / 迭代器模式 / ER模型 / 仓储模式,注意用Linq方法实现
多快好省!前端后端,线上线下,名师精讲
更多了解 加: