视频地址: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的元素取出并排序
普通写法
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();
查询表达式:
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();
链式表达式:
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 取两个数组的交集
普通写法
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();
查询表达式
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();
链式表达式
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 统计数组中数字的频率
普通写法
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();
查询表达式
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();
其中
new { Key = g.Key, Count = g.Count() }
为匿名类写法
链式表达式
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表达式在定义时不会真正的执行,只有在真正消耗时才会执行。
代码一:
var lst = new List<int> { 1, 2, 3, 4, 5 };
var query = lst.Select(x=>x*x);
lst.Add(6);
query.Dump();
运行结果为
代码二:
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 展平
将多维数组转为一维形式
查询表达式
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();
链式表达式
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 笛卡尔积
普通写法
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();
}
}
}
运行结果
查询表达式
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();
链式表达式
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 字母频率
查询表达式
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();
结果
链式表达式
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 批量下载文件
普通写法
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秒
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
改进写法
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秒
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
查询表达式
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();
}
链式表达式
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()
比如说一个集合处理完后,需要得到其第一个元素,使用以下写法
var arr = new List<int>{1,3,5,7,9,2,4,6,8,0};
arr.Select(x=>x).ToList()[0].Dump();
虽然也得到了结果,但是多占用了内存,而是应该尽量使用IEnumerable<T>自带的方法
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方法求平均数:
var arr = new List<int>{1,3,5,7,9,2,4,6,8,0};
(arr.Sum()/arr.Count).Dump();
使用自带方法Average
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
var arr = new List<int>{1,3,5,7,9,2,4,6,8,0};
arr.Where(x=>x%2==0).First().Dump();
可以把条件直接翻入First
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()的区别
比如说,查找年龄最大的对象
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就可简洁实现
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,等等
比如说以下代码
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后
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语句。
var arr = new List<int> { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
var res =
from x in arr
select x;
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语句。
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语句。
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
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 ...
基于特定值联接两个序列
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 …
基于特定值联接两个序列,并对每个元素的结果匹配项进行分组
IEnumerable<IEnumerable<Student>> studentGroups = from department in departments
join student in students on department.ID equals student.DepartmentID into studentGroup
select studentGroup;
studentGroups.Dump();