(十三) linq

1.什么是linq

image
不管是sql server还是mysql,我们都可以使用 ADO.NET 或者 entity framework来连接数据库,并将 SQL 命令发送到数据库来实现数据查询工作。

那么,如果这些数据保存在csv或者是xml文件中呢?那么我们也可以使用 .net 的文档阅读API来查询这些问数据。

对于不同的数据来源我们需要使用不同的数据处理流程,所有这些 API 都是不同的,并提供了不同级别、完全不一样的数据处理方式。
LINQ 为我们提供了强类型的编译时检查,可以对内存中数据、关系数据和 XML 数据执行的查询进行检查。
image

1.1 如何排序文件

命令行

C:\Windows>dir /os

c#

string path = @"C:\windows";
ShowLargeFiles(path);

方法ShowLargeFiles:

  • 使用DirectoryInfo类型,这个类型需要引用 System.IO 命名空间。然后再创建文件夹信息对象的时候把路径信息path传递进去。
    DirectoryInfo directory= new DirectoryInfo(path);
  • 而这个direactory将会包含文件夹内所有的信息,接下来,调用GetFiles方法,我们就可以以数组的形式获得这个文件夹中所有的文件。
    FileInfo[] files = directory.GetFiles();
  • 用一个foreach来循环输出文件夹内所有的文件了。Console 文件的名字 name 和文件长度 length。在console中我们做一个小处理,给文件名加上-20的空间,给文件大小、文件长度加上10个字符的空间,顺便加上:N0处理一下数字,给数字每三位加上个逗号,增加数字的可读性。
foreach (var f in files)
{
 Console.WriteLine($"{f.Name, -50} : {f.Length, 10:N0}");
}

距离的含义:
image

1.2 排序

我们需要先创建一个新的class用来处理文件对比,

  • class名称FileInfoComparer。
  • 它需要实现对象对比接口,IComparer,对比类型为FileInfo。
  • 使用vs自动实现接口,我们需要更改的就是这个Compare方法。
    internal class FileInfoComparer : IComparer<FileInfo>
    {
        public int Compare(FileInfo x, FileInfo y)
        {
            return y.Length.CompareTo(x.Length);
        }
    }

现在,我们就可以在sort方法内部通过实例化一个FileInfoComparer来完成文件列表的排序了。
image

1.3 linq

首先,我将会用ShowLargeFiles类似的名字创建一个新方法,不过用WithLinq结尾,代表这个方法将会用linq语法来处理文件搜索的业务逻辑。

linq的精髓其实就是把一个命令式 (imperative)的语法过程改为声明式(declarative)语法的过程。
image
image

像SQL一样
image
image
我们可以对比一下之前的代码,可以很明显的感觉到linq语法在进行数据处理的时候会更加友好,而且语法结构更接近对关系型数据库的操作理念。

而实际上,linq语法的强大之处绝对不仅限于次,刚刚我说过linq是一种结合了声明式的命令式语法结构,所以从底层实现上来说,它依然是命令式语法。既然依然是命令式语法,那么我们就依然可以通过对象的链式结构来完整的推到出数据结果。

var query = new DirectoryInfo(path).GetFiles().OrderByDescending(f => f.Length).Take(5);
也就是说,通过对象的链式表达,我们同样可以获得与声明式语法同样的效果,而且还能获得对象的关联关系。

2.lambda表达式

那么在c#中where运算符的基本用大就是向参数中传入一个方法,比如,我们现在传递的方法就叫做StartWithA,它的唯一作用就是判断字符串是否以A开头,返回值为布尔值。
image
匿名方法所建所用,随用随弃,它们就是一个一次性的方法,创建的同时将会同时被调用,调用完成以后马上就会被废弃。

在c#中有好几种匿名方法的创建。第一种就是使用delegate关键词,通过委托来创建匿名方法。delegate 关键词后面直接使用花括号用来表示匿名方法的参数,然后加上方法的处理逻辑。

像这种能在一个方法的参数中处理代码逻辑的处理方式叫做内联方法,inline,而内联的关键就是匿名方法。 这种内联方法可以极大的减轻代码的复杂度,减少代码行数,让代码维护更加方便简单。
image
不过,我们现在看到的这个匿名创建方法其实挺繁琐的,它的语法很嘈杂、有很多干扰正常业务逻辑的噪音。比如说,委托关键词 delegate,我们需要知道匿名方法创建的底层逻辑吗?明显不需要,参数s从哪里来呢?明显从客户列表中来,每个客户类型都是string,那么我们还有必要重复给参数s定义类型吗?花括号代表的是方法体,我们只有一行代码,干嘛还要花括号呢?既然不需要花括号,那么我们还要return关键词干嘛?为什么不能直接输出或者直接返回计算结果呢?

于是c#语言的设计者就在委托方法的基础上引入了lambda表达式。所谓,lambda表达式说白了还是一个匿名方法,他是基于数学的λ演算而得名。在C#中,我们只需要把刚刚我们提到的委托方法的干扰因素全部去掉,然后把return换成个箭头就可以了。
image

说白了,lambda表达式就等于箭头方法、就等于匿名方法。而lambda表达式这种语法结构不仅适用于 C# 中,在java、JavaScript、python中也非常常见。

演示:

var query = new DirectoryInfo(path).GetFiles()
                .OrderByDescending(f => f.Length)
                .Where(f => f.Name.StartsWith("b"))
                .Take(5);

3.linq的查询语法

image

  • 结构化查询总是以from关键字开头,这个from表达式想象成为很像一个 foreach 循环。
  • In 则代表我们需要引用的集合。
  • 而在查询语法的下一行,可以使用 where 关键字过滤这个客户列表,比如说,我们需要一个以“斯”为结尾的,并且总长度小于4个字的客户。
  • 我还可以将变量customer 与 orderby 关键字一起使用,按字母顺序来给查询结果排序
  • 查询语句的最后,还必须使用一个 select 关键字来告诉编译器我们需要的数据结果是什么。
  • 于是,最后这个查询语句的返回类型将会由最后的这个select内容所决定。所以,我们的查询结果将会是一个类型为字符串的集合。

我们的select直接决定了数据的类型,也就是数据的长相,这个过程我们称作数据塑性。而数据塑性也正是linq语法强大的地方,我们可以在select中随意对数据进行整理,可以原样返回,也可以给原数据添油加醋以后再返回,甚至返回一个完全不相干的数据都是完全可以的。
image

4.深度剖析linq的原理

image

public static class MyLinq
{
    public static IEnumerable<T> MyWhere<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        var result = new List<T>();
        foreach(var item in source)
        {
            if(predicate(item))
            {
                result.Add(item);
            }
        }
        return result;
    }
}
  • 方法的第一个参数是数据源, 与返回类型一直,IEnumerable source。但是在linq中我们需要使用对象的链式结构,所以,这里的第一个参数就变得很特殊了,这个参数其实就是Mylinq方法自己,this,或者说是上一个对象链式结构的处理结果。加上this以后,我们就可以通过调用方法来自动向参数加载数据对象了。

  • 接下来,第二个参数更为关键,还记得原where方法的参数吗,是一个lambda表达式对吧,而lambda表达式其实就是委托,所以,这里我们需要传入的第二个参数就是一个委托方法。这里可以使用Func类型,断言语句 predicate。断言带有判断的意思,所以,这里Func类型的第一个泛型类型是泛型T,而第二个泛型则应该是布尔类型。

  • 那么方法MyWhere的具体逻辑也不难,就是用一个for循环来查看source中的数据,如果source中的数据满足委托方法predicate的要求,那么我们就返回当前的数据,否则,啥都不返回。如果啥都不返回,就相当于把数据过滤掉了。

4.1 改进(延迟执行)

  • 从底层结构上来说,where语句利用了yeild return对数据进行了延迟加载,所以效率是非常高的。
  • 仿照where来使用yeild return来更新一下我们的MyWhere方法吧。
public static IEnumerable<T> MyWhere<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
   //var result = new List<T>();
   foreach(var item in source)
   {
       if(predicate(item))
       {
           //result.Add(item);
           yield return item;
       }
   }
   //return result;
}

5.读取csv文件

所谓csv实际上就是一个小型数据库,每行都是一组数据,数据的字段之间使用逗号隔开。

  • csv文件选择属性。在copy to output选项中,我们选择copy if newer。
  • 不过在读取文件之间,我们先来根据csv创建数据模型。请同学们右键点击项目名称,添加新class 文件。class名称 Car,首先给汽车类加上public访问修饰符。然后根据csv的字段,我们来定义数据模型的各种属性。

image
数据塑形
image

6.排序与过滤

接下来,我们来尝试一下获得综合油耗最低的10辆车,
直接对cars列表调用OrderByDescending,然后lambda表达式中以汽车的综合油耗 c.Combined 降序排列就可以了。
var query = cars.OrderByDescending(c => c.Combined);

foreach(var car in query.Take(10))
{
    Console.WriteLine($"{car.Model} {car.Combined}");
}

有一个非常重要的提醒,我们绝对不能连续使用orderby方法。虽然连续使用orderby没有任何语法错误,但是orderby会给整个列表进行重新排序,而不能执行次一级排序。

var query = cars.OrderByDescending(c => c.Combined)
                .OrderBy(c => c.Model);

运行代码,我们会发现这次的数据全部乱掉了,输出的结果是车辆型号的排序结果,油耗排序完全被忽略掉了。

所以,c#给我们提供了另一个API,叫做ThenBy。使用ThenBy替换OrderBy以后,我们的排序可以区分一级排序和二级排序的关系了。
ThenBy也有升序排列和降序排列两种类型,降序就叫做ThenByDescending

var query = cars.OrderByDescending(c => c.Combined)
                .ThenBy(c => c.Model);
var query = cars.OrderByDescending(c => c.Combined)
                .ThenByDescending(c => c.Model);

如果我们需要使用linq to sql的方式该怎么处理呢?其他部分都好说,有点难度的是这里的orderby,不过,声明式结构的代码反而比对象化代码更加简单,我们只需要在orderby语句中按顺序列出需要排序的字段,字段之间使用逗号分隔,linq语法就会自动识别一级排序和二级排序例了。

var query = from car in cars
                        orderby car.Combined descending, car.Model ascending
                        select car;

var query = from car in cars
            where car.Manufacturer == "BMW" && car.Year == 2016
            orderby car.Combined descending, car.Model ascending
            select car;

当只要第一名:
其实除了使用 take(1),我们还有一个更好的方法来取得第一名,这就是 first 以及 FirstOrDefault。

  • take(1)会返回一个集合
  • first 以及 FirstOrDefault返回一个对象
  • 如果列表为空,直接使用first方法会报错,而FirstOrDefault则会进行错误处理,返回一个空值。
  • 一般来说,我在工作中会倾向于使用FirstOrDefault,这样的代码会更加安全一点。

7.any all contains

any

any这个方法来查询csv数据库中是否存在大众的汽车。

var query2 = cars.Any(c => c.Manufacturer == "Volkswagen");
Console.WriteLine(query2);

可以看到query2的输出是布尔值 true。所以,通常这个方法会配合if语句来使用,作为数据集合的判断标准。

if (cars.Any(c => c.Manufacturer == "Volkswagen"))
{
    Console.WriteLine("有大众");
}
else
{
    Console.WriteLine("无大众");
}

contains

与 any 相似的,linq还有提供了一个叫做contains的API。这个contains方法的参数接受的不是lambda方法,而是一个实例对象。
cars.Contains(query);

all

与 any 相反,linq还有一个叫做all的API, 如果我们把any换成all,那么这里表示的是查询列表中是否所有的数据都是大众汽车。如果全都是,返回true,否则为false。

8.数据投影与SelectMany

在学习解析和读取csv文件的时候,我们的select语句把csv中的每一行数据都转换为了一个car对象,数据的形态发生了变化,这就是一个典型的数据投影的过程。

然后创建一个新的类,我们来定义一下这个ToCar方法。这个新类名称可以叫做CarExtensions,汽车拓展类,共有静态属性。

然后创建一个共有、静态、返回值为IEnumerable的方法ToCar。接下来的处理方式和我们之前处理自定义MyWhere方法非常相似,原理都是使用了c#的方法拓展原理。

它的第一个参数就是带有this关键词的为IEnumerable的数据源 sources。为什么参数的泛型类型为字符串呢?这是应为数据源其实是从csv中获得的,所以数据源实际上是csv每一行的字符串信息的列表集合。

因为数据源 sources是一个列表,所以,我们需要在一个foreach循环中对数据源的每一项都做出投影。所以 foreach (var line in source)

然后,可以复制粘贴刚刚注释掉的select语句中的内容。因为我们最终的返回数据是一个IEnumerable集合,所以,我们在foreach中直接使用 yield return 进行返回就可以了。直接yield return还可以实现延迟加载,提高程序的运行效率。

public static IEnumerable<Car> ToCar(this IEnumerable<string> source)
{
    foreach (var line in source)
    {
        var columns = line.Split(',');
        yield return new Car
        {
            Year = int.Parse(columns[0]),
            Manufacturer = columns[1],
            Model = columns[2],
            Displacement = double.Parse(columns[3]),
            Cylinders = int.Parse(columns[4]),
            City = int.Parse(columns[5]),
            Highway = int.Parse(columns[6]),
            Combined = int.Parse(columns[7])
        };
    }
}

数据投影的功能非常强大。尤其是我们需要一个数据子集的时候,数据投影往往可以有效的帮我们过滤很多无效数据。

比如说,在上面的query中,虽然csv定义了各种各样的字段,什么年份啊、品牌啊、引擎信息啊、油耗啊什么的。但我们在最后的数据输出中实际上只需要2个字段,就是汽车型号Model和汽车的油耗Combined。所以,对我们来说,其他的数据全部都是冗余数据,应该被删掉才对。

那么,使用select进行数据投影就可以帮我们解决这个麻烦。我们可以直接在select中使用new 关键词返回一个object类型的对象。这个对象只有两个字段,Model和Combined。

于是,我们的数据经过投影以后,最终剔除了多余的字段,只保留了接下来的有效数据。而在console中我们依然可以保留query对象对Model和Combined字段的语法高亮。

var query = (from car in cars
             where car.Manufacturer == "BMW" && car.Year == 2016
             orderby car.Combined descending, car.Model ascending
             //select car)
             select new
             {
                 Model = car.Model,
                 Combined = car.Combined
             })
            //.Take(1)
            //.First()
            .FirstOrDefault();

除了select语句,linq还提供了一个SelectMany的数据投影方式。这个SelectMany比较抽象,它的作用是展开嵌套的集合,并且提取子集合中所有的数据。

比如说,我们有一个汽车集合,而每辆汽车又坐着若干名乘客,请问我如何能获得乘客列表?

一般来说,对于这种情况,我们可以使用两个for循环就能解决问题。

不过,对于linq来说,使用for循环是不能接受的。更简单的方法是直接调用SelectMany
var allPassengers = carList.SelectMany(car => car.passengers);

9. join

与sql语法一样,join关键词可以把两个不同的数据源合并起来。
在join on的语法中我们不能使用等号,只能使用关键词equals来进行判断。而且,在join on的语法中,我们只能进行相等或不相等的判断,不能对比大小,所以大于小于号也是不能用的。
image


接下来,我们来尝试一下使用对象调用的方式来处理linq查询。
第一个lambda是cars列表中的对象每一辆车car,表示car对象的连接字段,car对象的连接字段是生产商名称,c.Manufacturer;

第二个lambada是生产商列表中的对象每一个厂家,表示厂家对象所需要使用的连接字段,这个字段就是生产商的名称,m.Name。

接下来,join方法还有最后一个参数,它表示的是连接以后最终的数据数据形式,它依然是个lambda表达式,表达式需要两个参数(c, m),c代表car对象,m代表生产商对象。我们可以直接实例化一个对象来暂时保存。

然后,油耗降序排列 OrderByDescending(c => c.Car.Combined);如果油耗相同的情况,按照车辆型号升序排列 ThenBy(c => c.Car.Model);

最后,使用select做数据投影,需要投影的数据分别是生产商Manufacturer, 汽车型号Model, 综合油耗Combined, 总部位置Headquarters,以及联系电话Phone。

如果同学们把鼠标放在select语句上,visual studio会告诉我们select语句目前需要的参数类型,需要被select数据其实就是join语句最后的输出。所以,参数可以叫做joinData。

var query2 = cars.Join(manufacturers, c => c.Manufacturer, m => m.Name, (c, m) => new
    {
        Car = c,
        Manufacturer = m
    })
    .OrderByDescending(c => c.Car.Combined)
    .ThenBy(c => c.Car.Model)
    .Select(joinData => new
    {
        Manufacturer = joinData.Car.Manufacturer,
        Model = joinData.Car.Model,
        Combined = joinData.Car.Combined,
        Headquarters = joinData.Manufacturer.Headquarters,
        Phone = joinData.Manufacturer.Phone
    });

10.group

inq语法非常灵活,它还提供了一种类似sql的数据分组的方法,方法名称同样也是group。我们可以通过group方法,来给数据分类。

group 是可以取代select对数据进行塑形的,所以再这种情况下,我们可以省略select。

var query = from car in cars
            group car by car.Manufacturer;

每一个分组都可以继续进行二次迭代,foreach car in 每个分组group,我们需要打印分组内油耗最低的前两辆汽车,所以,调用.OrderByDescending(c => c.Combined).Take(2)。然后console一下汽车信息,$" {car.Model} {car.Combined} "

foreach(var group in query)
{
    Console.WriteLine($"{group.Key} 有 { group.Count() } 量汽车。 ");
    foreach(var car in group.OrderByDescending(c => c.Combined).Take(2))
    {
        Console.WriteLine($"    {car.Model} {car.Combined} ");
    }
}

如果我希望我们的分组可以按照字母表升序来排序,该怎么处理呢?
排序就必须使用orderby,但是如果我们直接在query中使用orderby是没有办法建立分组信息与最终结果输出的。所以,对分组进行排序需要使用一个特殊的技巧,我们需要在完成分组group by以后加上 关键词 into,使用into关键词来引导对分组信息的引用,分组信息可以称作manufacturerGroup。那么现在,在orderby语句中我们就可以通过manufacturerGroup来设置排序信息了,我们需要排序的内容就是manufacturerGroup的key,也就是分组名称。

但是,orderby是不能作为linq语句的结尾的。linq语句的结尾只能使用select或者group by,所以,在查询语句的最后,我们还需要加上select,select的内容就是分组数据,manufacturerGroup。

var query = from car in cars
            group car by car.Manufacturer into manufacturerGroup
            orderby manufacturerGroup.Key
            select manufacturerGroup;

尝试一下使用对象化的结构来修改这坨linq查询。

var query2 = cars.GroupBy(c => c.Manufacturer).OrderBy(g => g.Key);

11.group join

group join就是把group和join两个操作结合起来,可以对多个数据源的数据进行分组

声明式结构

  1. 首先,第一个数据源从生产商中提取,from manufacturer in manufacturers; 然后,连接汽车数据 join car in cars,连接条件是生产商名称一致,on manufacturer.Name equals car.Manufacturer。

  2. 好了,数据连接完成。这个时候需要使用一个特殊的技巧,使用into关键词把汽车的分组信息提取出来,into carGroup。

  3. 然后,给分组排个序,顺序就是生产商的名称,orderby manufacturer.Name。

  4. 最后,通过select来完成最终的数据投影。select一个新对象,生产商Manufacturer 等于第一个数据源中得到的 manufacturer 对象;而汽车列表Cars 等于 汽车分组carGroup。
    image

把声明式结构改为对象的链式结构

  1. 我们使用生产商数据manufacturers列表来join汽车列表。不过,这次的join有点特殊,需要使用GroupJoin,也就是在join的同时对数据继续分组。

  2. 需要被join的列表是方法的第一个参数,汽车列表,cars。

  3. 接下来的两个参数都是lambda表达式,表示两段数据连接的字段。

  4. 第一个lambada是生产商列表中的对象每一个厂家,表示厂家对象所需要使用的连接字段,这个字段就是生产商的名称,m.Name。

  5. 第二个lambda是cars列表中的对象每一辆车car,表示car对象的连接字段,car对象的连接字段是生产商名称,c.Manufacturer;

  6. 最后一个参数,它表示的是连接以后最终的数据数据形式,依然是个lambda表达式,表达式需要两个参数(m, carGroup),m代表生产商对象manufacturer,carGroup表示这个厂家名下的汽车,我们可以直接实例化一个对象来暂时保存分组数据。

  7. 最后,对分组数据进行排序,OrderBy分组信息m的生产商的名称,m.Manufacturer.Name

12.数据聚合

还是以汽车数据为例,我希望能分别查看每个厂家的油耗水平,比如找出每一个品牌的平均油耗、最高有效、以及最低油耗。

我们希望排序的是经过数据变化的select出来的Avg平均油耗。那么我们可以使用 into 关键词,把select出来的数据先暂时保存到一个本地变量中,变量名称可以叫做ManufacturerGroup。

然后,我们根据这个ManufacturerGroup的avg字段来排序就好了。 orderby ManufacturerGroup.Avg, 降序排列 descending

最后,因为orderby不能作为linq的结尾,所以,我们还需要手动select一下输出数据。因为我们仅仅做了排序,并没有进行数据变换,所以,直接select ManufacturerGroup,输出ManufacturerGroup就好了。

var query = from car in cars
            group car by car.Manufacturer into carGroup
            select new
            {
                Manufacturer = carGroup.Key,
                Max = carGroup.Max(c => c.Combined),
                Min = carGroup.Min(c => c.Combined),
                Avg = carGroup.Average(c => c.Combined),
            } into ManufacturerGroup
            orderby ManufacturerGroup.Avg descending
            select ManufacturerGroup;
posted @ 2023-11-29 09:12  huihui不会写代码  阅读(85)  评论(0)    收藏  举报