0
点赞
收藏
分享

微信扫一扫

c#入门-LINQ表达式语法

b91bff6ffdb5 2022-02-09 阅读 100
c#

注意: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);
}
举报

相关推荐

0 条评论