学编程,来源栈;先学习,再交钱
当前系列: C#语法 修改讲义

集合的增删改查中,最有技术含量的就是:查。

在过去很长一段时间里,我们只能在for/foreach循环中进行遍历和筛选,直到出现了


Linq

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}},
    };


Where

先睹为快,条件过滤,取出所有成绩(Score)大于90的学生(Student):

var excellent = students.Where(s => s.Score > 90);

F12演示

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);

MimicWhere()方法

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避免额外的集合声明


OrderBy

对集合中元素进行排序。需指定排序依据:

  • 按成绩从小到大:
    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>());

ThenBy()

实现先按某字段(比如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方法立即进行查询运算的方法,常见的有:

ToXXX

ToList()/ToArray()/ToXXX等方法:将IEnumerable转换成List/数组/其他集合对象

List<Student> list = excellents.ToList();
Student[] array = excellents.ToArray();

单个元素

First()/Single()/Last(),获取第一个/唯一一个/最后一个元素。

演示:如果说

  • 不止一个元素的话,Single()会报错
  • 一个都没有的话,First()和Last()会报错
如果不想抛异常的话,可以加后缀OrDefault:
Console.WriteLine(excellents.SingleOrDefault()?.Name);
意思是:如果没有符合条件的元素,返回该元素类型的默认值
  • 引用类型null值复习
  • 其他(0,false……)

聚合函数

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));

为什么?

两个原因:

  • 保证获取的是“即时”(up-to-date)数据
    //假设students(数据源)发生了变化
    students.Add(new Student { Score = 65, Name = "" });
  • 在进行多条件查询拼接时提高性能,使用最终表达式一次遍历,完成所有查询


Take和Skip

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()的数量超过了集合中元素数量,甚至都不报错……有意思!


GroupBy

比如说我们要通过majors统计每个老师上了多少门课,首先就要

分组,把所有的Majors按其授课老师分组:

var groupedMajor = majors.GroupBy(m => m.Teacher);

分组过后得到的结果是什么?IEnumerable<IGrouping<Teacher, Major>>,其关键是:

  • IGrouping<Teacher, Major>里的Key(Teacher:分组依据)
  • 迭代IGrouping<Teacher, Major>获得(iterate)的值(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 });

ToDictionary()

更多的时候,我们是利用聚合函数进行统计,得到各个小组的:数量/最大/最小/和/平均值等……

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());


Select

投影:可以将集合中每个元素的一个或多个属性抽取出来,再组合成一个新集合。

单个属性

IEnumerable<double> scores = students.Select(s => s.Score);

注意不要混淆:select和where

  • where是“横向”的操作(一个学生一行),比如从10个学生中取出3个年龄大于20岁的学生,取出来的还是学生;
  • select是“纵向”的操作(学生的一个属性算一列),比如取出学生的姓名和年龄,取出来的就不是学生了,而是一个属性或者多个属性的组合

多个属性

可以使用

已声明类型

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);


Join

@想一想@:如何获取一个集合,显示每个老师的姓名、年龄和所教授的课程?

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表达式有表现力,所以此处只简单介绍。


SelectMany

需求:找出所学课程名包含了字母s(不论大小写)的学生,将学生及其对应的课程作为集合元素存储

注意这几个数据:

  • C#是没有包含s的
  • 添加一个包含s但没人学的CSS
    Major Css = new Major { Name = "CSS" };

#理解#必须同时满足两个条件:

  • 课程有人学
  • 课程名字中包含s

思路就是先把所有学生的所有课程取出来,将之前的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方法实现

学习笔记
源栈学历
键盘敲烂,月薪过万作业不做,等于没学

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 C#语法 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码