注意:LINQ内容有方法版本的语法。但本节教程只教语法和格式,不考虑实用性
(并不是说查询语法不好,使用方法语法对部分人来说看的更习惯)。
数据源
在编写程序时常常要对数据进行处理。LINQ就是用于对数据集查找,筛选数据的指令。
数据源需要是实现了IEnumerable接口,或他的泛型版本IEnumerable<T>。
或者有GetEnumerator方法。这个方法是IEnumerable接口下的,但是为了兼容扩展方法,编译器会直接按照方法名字查询。
具有上述特征的数据类型是数据集,可以遍历,迭代。例如数组就实现了这个接口。
此外,这些数据可以作为string.join方法的参数(因为具有重载,所以这个方法没有写成IEnumerable的扩展方法).
第一个参数是连接元素的字符串,第二个参数是数据源
映射
查询语法
LINQ的表达式语法首先需要获得数据源。语法为
form 元素名 in 数据源
元素名是你现场声明的名字,不需要和数据源中的名字一样。
数据源需要是一个对象,不能是类型。
映射表达式可以把源数据的类型按你的方法处理,然后再打包成一个数据源给你。
int[] a = new int[100];
var b = from num in a
select num+20;
不过打包回来的类型是IEnumerable接口,如果想变成数组则要调用ToArray方法
int[] c = b.ToArray();
如果想在表达式后直接调用,则要为表达式打上括号
元素映射
元素名是为了方便你获取元素的属性或调用他的方法。你可以映射出和源数据完全无关的值
int[] a = new int[100];
Random r = new Random();
var b = (from num in a select r.Next(100)).ToArray();
映射出值没有类型要求,只要你映射出的不是void就行。
例如你有一个学生类的数组
Student[] students = { new("小明", 12), new("小红", 13), new("小丽", 14) };
record Student(string name, int age);
你希望获得所有学生的名字。
匿名类
匿名类是没有预先定义的类。声明匿名类必须要使用var关键字。
匿名类不能存在方法。甚至就是用引用元组实现的(之前讲的元组是值元组)。并且所有字段都是只读的。
匿名类的一个用处就是在LINQ的查询中,希望返回多个值时使用。
Student[] students = { new("小明", 12), new("小红", 13), new("小丽", 14) };
int i = 100;
var c = from stu in students
select new { name = stu.name, age = stu.age, id = i++ };
但是值元组可以做到这些事:
Student[] students = { new("小明", 12), new("小红", 13), new("小丽", 14) };
int i = 100;
var c = from stu in students
select (name: stu.name, age: stu.age, id: i++);
筛选
在映射之前可以先用where过滤出符合条件的数据
查询语句必须以from开头,以映射或分组为结尾。
var c = from stu in students
where stu.age > 12
select stu.name;
Console.WriteLine(string.Join(",",c));
同样的,筛选的条件可以不用到源数据,例如也使用随机数判断。
你可以使用模式匹配来进行复杂的逻辑筛选
where stu is { name.Length: 2, age: > 12 }
当然也可以使用正常的逻辑运算,或者使用多个where进行筛选。
分组
首先更换以下数据源。以下模型指示运动员射击练习时的分数和他本人的名字。
Record[] records = { new("小明", 3), new("老王", 6), new("小刚", 8),new("小刚",7),new("老王",5),
new("小明",10),new("小明",4),new("小刚",7),new("小刚",6),new("老王",9)};
record Record(string name, int scores);
使用分组,可以把具有相同键的数据打包。分组和映射只能用其中一个作为结尾(除非这个查询是嵌套的)。
分组使用group关键字,指示希望打包的数据。然后跟随by,指示分组依据
var v = from rec in records
group rec by rec.name;
此时的元素类型是IGrouping。除了继承了IEnumerable以外,自己的属性只有一个Key,表示共有键。
foreach (var item in v)
{
var s = from i in item select i.scores;
Console.WriteLine(item.Key + ":" + string.Join(", ", s));
}
类似的,你也可以使用和数据源完全无关的条件分组,例如希望将这个数据分成三份
int i = 0;
var v = from rec in records
group rec by i++ % 3;
排序
在映射或分组前,可以使用排序子句。使用orderby关键字。指示排序的映射依据
var v = from rec in records
orderby rec.scores
select (rec.scores,rec.name);
Console.WriteLine(string.Join(",", v));
映射后的依据必须实现了比较接口。同样的,映射方式可以和数据源无关。
例如Guid类是用于生成序列号的,他有一个静态方法可以返回一个随机的序列号。他自己实现了排序接口
因此,在用于打乱顺序时,例如歌单随机播放。可以使用这个作为排序依据。
(当然直接用随机数也可以,但是如果在外面new Random就要多写一行,在里面现场new Random会造成更大的资源开销)
排序默认为升序排序,即小的在前面,大的在后面。
在排序依据后加上ascending关键字可以强调这个意图。
或者加上descending关键字改为降序排序
别名
分组依据,映射数据都会生成一个新的序列。这个序列可以命名并在后续使用。命名他们使用into关键字。
例如,希望在分组完成后对分组进行排序。
var v = from rec in records
group rec by rec.name into name
orderby name.Key
select name;
例如,在映射出元素后希望把元素的属性打包成元组,或作为方法的参数使用
var v = from rec in records
select rec.name into o
select (o.Length, new System.Text.StringBuilder(o));
嵌套子句
作为查询用的数据源本身就可能是一个查询出来的结果。
如果希望不用变量储存临时结果那么就需要使用嵌套的子句。
例如:从数组的数组中进行查询
int[][] i = { new int[] { 2, 3 }, new int[] { 4, 6 }, new int[] { 5, 8 } };
var t = from num in i
from num2 in num
select num2;
Console.WriteLine(String.Join(",", t));
不经过映射直接多次from会把数据源串联起来。
再例如,在上面的数据中,希望找出每个人最好的成绩。那么就要先分组,再进行排序和映射。
var v = from rec in
from rec1 in records
group rec1 by rec1.name
select (from rec2 in rec
orderby rec2.scores
select rec2).Last() into o
select (o.name, o.scores);
Console.WriteLine(string.Join(",", v));
这个过程中调用了IEnumerable的扩展方法:获得序列的最后一个元素。
对升序排列的元素来说,最后一个元素就是最大的元素。
再例如,在按照主人归类成绩后,再按照姓氏分组
var v2 = from rec2 in
from rec in records
group rec by rec.name
group rec2 by rec2.Key[0];
联表
如果你需要的数据分散在多个数据源中。例如:
会员类储存了会员的名字和id
货物类储存了货物的名字,价格,id
订单类储存了会员的id,货物的id,购买的数量。
你希望生成一个数据集,表示谁,买了什么,一共付了多少钱。
首先生成数据模型
VIP[] vips = { new("小明", 1), new("小红", 2), new("小王", 3), new("小丽", 4) };
Item[] items = { new("西瓜", 1, 5), new("苹果", 2, 3), new("香蕉", 3, 4), new("葡萄", 4, 2) };
Order[] orders = { new(1, 2, 1), new(2, 2, 3), new(3, 1, 2), new(3, 2, 1), new(1, 3, 1) };
record VIP(string name, int id);
record Item(string name, int id, int price);
record Order(int VIPId, int ItemId, int num);
内连接
在连接不同的数据源时,需要指定数据源中匹配的键。
执行内连使用join语句,并使用on指定相等的键,使用equls连接左右匹配的键。
(两个数据源需要处于equls的两边,所以使用关键字隔开而不适用==比较。并且equls会调用equls方法)
var vo = from vip in vips
join ord in orders on vip.id equals ord.VIPId
select (vip.name, ord.num, ord.ItemId);
foreach (var (name, num, id) in vo)
{
Console.WriteLine($"{name}买了{num}件id为{id}的物品");
}
再执行一次连接,就可以获得完整的信息
var voi = from vip in vips
join ord in orders on vip.id equals ord.VIPId
join ite in items on ord.ItemId equals ite.id
select (vip.name, ord.num, ite.name, ite.price);
foreach (var (name, num, ite, pri) in voi)
{
Console.WriteLine($"{name}买了{num}件{ite},花费{pri * num}元钱");
}
组合键连接
虽然在大部分场景,唯一标识都只依赖于一项数据。例如身份证号,QQ号。
但是有小部分场景中,唯一标识可能由多项数据共同构成,例如暴雪的用户ID。
暴雪的ID不像QQ号,每个用户都有独一无二的号码,而是在同名用户中拥有独一无二的号码。
因此想要无歧义地找到一个用户,需要同时拥有他的用户名,和他的ID。
在这种情况下连接表需要组合键匹配。
虽然说了这么多,结果官网介绍用的例子就是直接用匿名类打包起来比较。
分组连接
into别名可以在执行内联时对join使用。在映射时可以把具有匹配的连接数据直接进行分组。
var vojoin = from vip in vips
join ord in orders on vip.id equals ord.VIPId into gro
select (vip.name, gro);
foreach (var (name ,gro) in vojoin)
{
Console.Write(name+":");
Console.WriteLine(string.Join(",",gro));
}
左外部连接
左外部,右外部,全外部连接是数据库查询语句中的概念。
外部连接的一方会返回所有数据,即便在另一边没有匹配项。(分组连接也可以算左外部连接)
c#中的左外部连接方式是在分组连接的基础上,为没有数据的分组源中添加一项默认数据,让他可以在查询结果时显示出来。
var vojoin = from vip in vips
join ord in orders on vip.id equals ord.VIPId into gro
from sub in gro.DefaultIfEmpty()
select (vip.name, sub);
foreach (var (name, gro) in vojoin)
{
Console.Write(name + ":");
Console.WriteLine(string.Join(",", gro));
}
DefaultIfEmpty方法的参数可以填写你指定的默认值。
交错匹配
在执行连接时,如果匹配键写与数据源无关的,恒等的数据。就会把一个源中的所有项和另一个源中的所有项全匹配。
这种连接可以直接使用多个from句子完成
string[] 花色 = { "黑桃", "红桃", "梅花", "方块" };
string[] 数字 = { "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K" };
var 扑克 = from a in 花色
from b in 数字
select a + b;
foreach (var item in 扑克)
{
Console.WriteLine(item);
}