[C#学习笔记]LINQ

视频地址:LINQ入门示例及新手常犯的错误_哔哩哔哩_bilibili

强烈推荐学习C#和WPF的朋友关注此UP,知识点巨多,讲解透彻!

一、基本概念

语言集成查询(Language-Intergrated Query)

常见用途

  • .Net原生集合(List,Array,Dictionary,etc.)
  • SQL数据库(尤其搭配ORM)
  • XML文档
  • JSON文档(Newtonsoft.Json)

常见功能

  • 排序、筛选、选择
  • 分组、聚合、合并
  • 最大值,最小值,求和,求平均,求数量
  • ......

两种形式

  • 查询表达式 query expression
  • 链式表达式 chained expression

例如,现在有个List<int>,内容为0-9,无序排列,需要把其中大于等于4的元素取出并排序

普通写法

cs 复制代码
var lst = new List<int> {1,3,5,7,9,2,4,6,8,0};

var res = new List<int>();
foreach (var n in lst)
{
	if (n %2 == 0 && n >= 4) res.Add(n);
}

res.Sort();
res.Dump();

查询表达式:

cs 复制代码
var lst = new List<int> {1,3,5,7,9,2,4,6,8,0};

var res = from n in lst
	where n % 2== 0 && n >= 4
	orderby n
	select n;

res.Dump();

链式表达式:

cs 复制代码
var lst = new List<int> {1,3,5,7,9,2,4,6,8,0};

var res = lst
.Where(n=> n%2 == 0 && n>= 4)
.OrderBy(n=> n);

res.Dump();

二、例程

2.1 取两个数组的交集

普通写法

cs 复制代码
var arr1 = new int[]{1,2,3,4,5,6};
var arr2 = new int[]{4,5,6,7,8,9};

var res = new List<int>();
foreach (var n in arr1)
{
	if (arr2.Contains(n)) res.Add(n);
}

res.Dump();

查询表达式

cs 复制代码
var arr1 = new int[]{1,2,3,4,5,6};
var arr2 = new int[]{4,5,6,7,8,9};

var res = from n in arr1 
	where arr2.Contains(n)
	select n;

res.Dump();

链式表达式

cs 复制代码
var arr1 = new int[]{1,2,3,4,5,6};
var arr2 = new int[]{4,5,6,7,8,9};

var res = arr1
	.Intersect(arr2);

res.Dump();

2.2 统计数组中数字的频率

普通写法

cs 复制代码
var rnd = new Random(1334);
var arr = Enumerable.Range(0, 200).Select(_ => rnd.Next(20));

var dic = new Dictionary<int, int>();
foreach (var n in arr)
{
	if (dic.TryGetValue(n, out int value))
	{
		dic[n] = value +1;
	}
	else 
	{
		dic[n] = 1;
	}
}

dic.Dump();

查询表达式

cs 复制代码
var rnd = new Random(1334);
var arr = Enumerable.Range(0, 200).Select(_ => rnd.Next(20));

var dic = 
	from n in arr
	group n by n into g
	select new { g.Key, Count = g.Count() };

dic.Dump();

其中

cs 复制代码
new { Key = g.Key, Count = g.Count() }

为匿名类写法

链式表达式

cs 复制代码
var rnd = new Random(1334);
var arr = Enumerable.Range(0, 200).Select(_ => rnd.Next(20));

var dic = arr
	.GroupBy(x => x)
	.Select(g => new {Key = g.Key, Count = g.Count()});

dic.Dump();

三、重要概念

3.1 延迟执行

LINQ表达式在定义时不会真正的执行,只有在真正消耗时才会执行。

代码一:

cs 复制代码
var lst = new List<int> { 1, 2, 3, 4, 5 };
var query = lst.Select(x=>x*x);

lst.Add(6);
query.Dump();

运行结果为

代码二:

cs 复制代码
var lst = new List<int>{1,2,3,4,5};

Stopwatch watch = new Stopwatch();
watch.Start();
var query = lst.Select(x =>
{
	Thread.Sleep(500);
	return x*x;
});

watch.Dump();
query.ToList();
watch.Dump();

运行结果

通过这两个代码可以看出,如果没有对查询表达式进行消耗的操作,表达式并不会真正执行

3.2 消耗

  • 遍历 foreach
  • ToList()、ToArray()、ToDictionary()、
  • Count()、Min()、Max()
  • Take()、First()、Last()

3.3 LINQ并不仅仅是可枚举类型的扩展方法

  • IEnumerable
  • IOrderedEnumerable
  • IQueryable
  • ParallelQuery

四、扩展例程

4.1 展平

将多维数组转为一维形式

查询表达式

cs 复制代码
var mat = new int[][]{
	new [] {1,2,3,4},
	new [] {5,6,7},
	new [] {8,9,10,11,12}
};

var res = 
	from row in mat
	from data in row
	select data;
	
res.Dump();	

链式表达式

cs 复制代码
var mat = new int[][]{
	new [] {1,2,3,4},
	new [] {5,6,7},
	new [] {8,9,10,11,12}
};

var res = mat
	.SelectMany(x=>x);
	
res.Dump();	

运行结果

4.2 笛卡尔积

普通写法

cs 复制代码
for (int i = 0; i < 5; i++)
{
	for	(int j = 0; j < 4; j++)
	{
		for (int k = 0; k < 3; k++)
		{
			$"{i},{j},{k}".Dump();
		}
	}
}

运行结果

查询表达式

cs 复制代码
var prods = 
	from i in Enumerable.Range(0, 5)
	from j in Enumerable.Range(0, 4)
	from k in Enumerable.Range(0, 3)
	select $"{i},{j},{k}";
	
prods.Dump();

链式表达式

cs 复制代码
var prods = Enumerable
	.Range(0, 5)
	.SelectMany(r => Enumerable.Range(0, 4), (l,r)=>(l,r))
	.SelectMany(r => Enumerable.Range(0, 3), (l,r)=>(l.l, l.r, r))
	.Select(x=>x.ToString());
	
prods.Dump();

4.3 字母频率

查询表达式

cs 复制代码
var words = new string[]{"tom", "jerry", "spike", "tyke", "butch", "quacker"};

var query = 
	from w in words
	from c in w
	group c by c into g
	select new {g, Count=g.Count()} into a
	orderby a.Count descending
	select a;

query.Dump();

结果

链式表达式

cs 复制代码
var words = new string[]{"tom", "jerry", "spike", "tyke", "butch", "quacker"};

var query = words
	.SelectMany(x=>x)
	.GroupBy(x=>x)
	.Select(x => new { x, Count = x.Count() })
	.OrderByDescending(x => x.Count);
	
query.Dump();

4.4 批量下载文件

普通写法

cs 复制代码
var urls = new string[]
{
	"http://www.example.com/pic1.png",
	"http://www.example.com/pic2.png",
	"http://www.example.com/pic3.png"
};

$"Tasks start at {DateTime.Now.ToString("HH:mm:ss ffff")}".Dump();
foreach (var url in urls)
{
	await DownloadAsync(url, url.Split('/').Last());
}
$"Tasks end at {DateTime.Now.ToString("HH:mm:ss ffff")}".Dump();


async Task DownloadAsync(string url, string filename)
{
	await Task.Delay(1000);
	$"{filename} downloaded at {DateTime.Now.ToString("HH:mm:ss ffff")}.".Dump();
}

输出结果,可以看到3个任务花了3秒

cs 复制代码
Tasks start at 16:48:19 7820
pic1.png downloaded at 16:48:20 7829.
pic2.png downloaded at 16:48:21 7841.
pic3.png downloaded at 16:48:22 7844.
Tasks end at 16:48:22 7845

改进写法

cs 复制代码
var urls = new string[]
{
	"http://www.example.com/pic1.png",
	"http://www.example.com/pic2.png",
	"http://www.example.com/pic3.png"
};

var tasks = new List<Task>();
foreach (var url in urls)
{
	tasks.Add(DownloadAsync(url, url.Split("/").Last()));
}

$"Tasks start at {DateTime.Now.ToString("HH:mm:ss ffff")}".Dump();
await Task.WhenAll(tasks);
$"Tasks end at {DateTime.Now.ToString("HH:mm:ss ffff")}".Dump();


async Task DownloadAsync(string url, string filename)
{
	await Task.Delay(1000);
	$"{filename} downloaded at {DateTime.Now.ToString("HH:mm:ss ffff")}.".Dump();
}

运行结果,可以看到3个任务花了1秒

cs 复制代码
Tasks start at 16:47:18 1255
pic1.png downloaded at 16:47:19 1259.
pic3.png downloaded at 16:47:19 1259.
pic2.png downloaded at 16:47:19 1259.
Tasks end at 16:47:19 1265

查询表达式

cs 复制代码
var urls = new string[]
{
	"http://www.example.com/pic1.png",
	"http://www.example.com/pic2.png",
	"http://www.example.com/pic3.png"
};

var tasks = 
	from url in urls
	select DownloadAsync(url, url.Split("/").Last());

$"Tasks start at {DateTime.Now.ToString("HH:mm:ss ffff")}".Dump();
await Task.WhenAll(tasks);
$"Tasks end at {DateTime.Now.ToString("HH:mm:ss ffff")}".Dump();

async Task DownloadAsync(string url, string filename)
{
	await Task.Delay(1000);
	$"{filename} downloaded at {DateTime.Now.ToString("HH:mm:ss ffff")}.".Dump();
}

链式表达式

cs 复制代码
var urls = new string[]
{
	"http://www.example.com/pic1.png",
	"http://www.example.com/pic2.png",
	"http://www.example.com/pic3.png"
};

var tasks = urls 
	.Select(url => DownloadAsync(url, url.Split("/").Last()));
	
$"Tasks start at {DateTime.Now.ToString("HH:mm:ss ffff")}".Dump();
await Task.WhenAll(tasks);
$"Tasks end at {DateTime.Now.ToString("HH:mm:ss ffff")}".Dump();

async Task DownloadAsync(string url, string filename)
{
	await Task.Delay(1000);
	$"{filename} downloaded at {DateTime.Now.ToString("HH:mm:ss ffff")}.".Dump();
}

五、避坑集合

5.1 尽量使用自带方法

5.1.1 First()、Last()

比如说一个集合处理完后,需要得到其第一个元素,使用以下写法

cs 复制代码
var arr = new List<int>{1,3,5,7,9,2,4,6,8,0};

arr.Select(x=>x).ToList()[0].Dump();

虽然也得到了结果,但是多占用了内存,而是应该尽量使用IEnumerable<T>自带的方法

cs 复制代码
var arr = new List<int>{1,3,5,7,9,2,4,6,8,0};

arr.Select(x=>x).First().Dump();

5.1.2 Average()

不要使用Sum+Count方法求平均数:

cs 复制代码
var arr = new List<int>{1,3,5,7,9,2,4,6,8,0};

(arr.Sum()/arr.Count).Dump();

使用自带方法Average

cs 复制代码
var arr = new List<int>{1,3,5,7,9,2,4,6,8,0};

arr.Average().Dump();

5.1.3 Count()、First()、Min()、Max()等方法可以传参

比如查找第一个偶数

使用Where+First

cs 复制代码
var arr = new List<int>{1,3,5,7,9,2,4,6,8,0};

arr.Where(x=>x%2==0).First().Dump();

可以把条件直接翻入First

cs 复制代码
var arr = new List<int> { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };

var res = 
	from x in arr
	select x;

5.1.4 Max()和MaxBy()的区别

比如说,查找年龄最大的对象

cs 复制代码
var people = new List<Person>
{
	new ("Tom", 18),
	new ("Jerry", 19),
	new ("Nason", 20),
};

var age = people.Max(x=> x.Age);
var p = people.First(x=>x.Age == age);
p.Dump();

record Person(string Name, int Age);

其实使用MaxBy就可简洁实现

cs 复制代码
var people = new List<Person>
{
	new ("Tom", 18),
	new ("Jerry", 19),
	new ("Nason", 20),
};

var p = people.MaxBy(x=>x.Age);
p.Dump();

record Person(string Name, int Age);

5.1.5 Default的使用

First和FirstOrDefault,Last和LastOrDefault,等等

比如说以下代码

cs 复制代码
var people = new List<Person>
{
	new ("Tom", 18),
	new ("Jerry", 19),
	new ("Nason", 20),
};

if (people.Any(x=>x.Age >= 21))
	people.First(x=>x.Age >= 21).Dump();

record Person(string Name, int Age);

使用FirstOrDefault后

cs 复制代码
var people = new List<Person>
{
	new ("Tom", 18),
	new ("Jerry", 19),
	new ("Nason", 20),
};

people.FirstOrDefault(x=>x.Age >= 21).Dump();

record Person(string Name, int Age);

5.2 注意开销

5.2.1 滥用ToList(),arr.Where().OrderBy().ToList()[0]

非必要不使用ToList,因为会多占用空间

5.2.2 滥用Count(),Count()>0

在判断集合中是否存在符合某个条件的元素时,应该使用Any(),

因为Count()是需要遍历所有元素,Any()是遇到第一个符合条件的就返回(最极端时才会遍历所有元素)

5.2.3 滥用OrderBy(),不适用Sort

OrderBy().ToList()需要重新开辟内存空间,Sort()是在原有集合上排序,不会多增加空间

而且OrderBy(x=>x)使用了lamda表达式,执行时间更长

5.2.4 不知道First()与Single()的区别

First是返回第一个符合条件的元素(可能有多个)

Single是有且只有一个

6. LINQ个人总结

6.1 命名

6.1.1 from

from后面是变量及变量的命名,作用域为当前LINQ语句。

cs 复制代码
var arr = new List<int> { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
var res = 
	from x in arr
	select x;
cs 复制代码
var res = 
	from x in Enumerable.Range(0, 5)
	from y in Enumerable.Range(10, 15)
	from z in Enumerable.Range(20, 25)
	select new {First = x, Second = y, Third = z} ;
	
res.Dump();

6.1.2 into

from前面是变量,后面是变量的命名,作用域为当前LINQ语句。

cs 复制代码
var query =
	from x in Enumerable.Range(0, 5)
	select new { x, Square = x * x} into y
	select y;
	
query.Dump();

6.1.3 let

let后面等号前为变量名,等号后为变量值,作用域为当前LINQ语句。

cs 复制代码
var query =
	from x in Enumerable.Range(0, 5)
	select new { x, Square = x * x} into y
	let result = y.Square
	select result;
	
query.Dump();

6.2 结尾

LINQ语句结尾必须要是select

6.3 Join

Join 个操作 - C# | Microsoft Learn

cs 复制代码
var students = new List<Student>()
{
	new (){ FirstName = "Bruce", LastName = "Cambell", ID = 10, Year = GradeLevel.FirstYear, DepartmentID = 223},
	new (){ FirstName = "Cindy", LastName = "Haneline", ID = 11, Year = GradeLevel.SecondYear, DepartmentID = 300},
	new (){ FirstName = "Andrea", LastName = "Deville", ID = 12, Year = GradeLevel.ThirdYear, DepartmentID = 400},
};

var teachers = new List<Teacher>()
{
	new (){ First = "Anita", Last = "Ryan", ID = 32, City = "NewYork" },
	new (){ First = "George", Last = "Bunkelman", ID = 44, City = "Washington"},
	new (){ First = "Andrew", Last = "Carter", ID = 50, City = "LosAngeles"}
};

var departments = new List<Department>()
{
	new (){ Name = "Secretariat", ID = 500, TeacherID = 32},
	new (){ Name = "General Office", ID = 300, TeacherID = 44},
	new (){ Name = "Law Committee", ID = 400, TeacherID = 50},
};

public enum GradeLevel
{
    FirstYear = 1,
    SecondYear,
    ThirdYear,
    FourthYear
};

public class Student
{
	public string FirstName { get; init; }
	public string LastName { get; init; }
	public int ID { get; init; }
	public GradeLevel Year { get; init; }
	public int DepartmentID { get; init; }
}

public class Teacher
{
	public string First { get; init; }
	public string Last { get; init; }
	public int ID { get; init; }
	public string City { get; init; }
}
public class Department
{
	public string Name { get; init; }	
	public int ID { get; init; }
	public int TeacherID { get; init; }
}

6.3.1 Join ... in ... on ... equals ...

基于特定值联接两个序列

cs 复制代码
var query = from student in students
			join department in departments on student.DepartmentID equals department.ID
			select new { Name = $"{student.FirstName} {student.LastName}", DepartmentName = department.Name };
			
query.Dump();

6.3.2 join ... in ... on ... equals ... into ...

基于特定值联接两个序列,并对每个元素的结果匹配项进行分组

cs 复制代码
IEnumerable<IEnumerable<Student>> studentGroups = from department in departments
												  join student in students on department.ID equals student.DepartmentID into studentGroup
												  select studentGroup;
studentGroups.Dump();
相关推荐
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
sanguine__5 小时前
Web APIs学习 (操作DOM BOM)
学习
冷眼看人间恩怨5 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
向宇it7 小时前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
数据的世界017 小时前
.NET开发人员学习书籍推荐
学习·.net
四口鲸鱼爱吃盐7 小时前
CVPR2024 | 通过集成渐近正态分布学习实现强可迁移对抗攻击
学习
向宇it8 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
OopspoO9 小时前
qcow2镜像大小压缩
学习·性能优化
A懿轩A9 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
居居飒10 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin