视频地址: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 lstwhere n % 2== 0 && n >= 4orderby nselect 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 arrgroup n by n into gselect 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 matfrom data in rowselect 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 wordsfrom c in wgroup c by c into gselect new {g, Count=g.Count()} into aorderby a.Count descendingselect 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 urlsselect 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 arrselect 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 arrselect 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 yselect 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 ylet result = y.Squareselect 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 studentsjoin department in departments on student.DepartmentID equals department.IDselect 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 departmentsjoin student in students on department.ID equals student.DepartmentID into studentGroupselect studentGroup;
studentGroups.Dump();