LINQ 是 Language INtegrated Query 单词的首字母缩写,翻译过来是语言集成查询。它为查询跨各种数据源和格式的数据提供了一致的模型,所以叫集成查询。由于这种查询并没有制造新的语言而只是在现有的语言基础上来实现,所以叫语言集成查询。
XML读取 1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" encoding="utf-8" ?> <Employees> <Employee> <EmpId>1 </EmpId> <Name>Liam</Name> <Sex>男</Sex> </Employee> <Employee> <EmpId>2 </EmpId> ... </Employee> </Employees>
1 var els = xelement.Elements("Employee" ).Where(el => (string)el.Element("Sex" ) == "Male" );
Except 取差集 LINQ 的 Except 方法用来取差集,即取出集合中与另一个集合所有元素不同的元素。
1 2 3 4 int [] first = { 1 , 2 , 3 , 4 };int [] second = { 0 , 2 , 3 , 5 }; IEnumerable<int > result = first.Except(second);
对于简单类型(int、float、string 等)使用 Except 很简单,但对于自定义类型(或者叫复合类型,下同)的 Object 如何使用 Except 呢?
此时需要将自定义类型实现IEquatable接口,示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class User : IEquatable<User> { public string Name { get; set; } public bool Equals (User other) { return Name == other.Name; } public override int GetHashCode () { return Name?.GetHashCode() ?? 0 ; } }class Program { static void Main (string[] args) { var list1 = new List <User> { new User { Name = "User1" }, new User { Name = "User2" }, }; var list2 = new List <User> { new User { Name = "User2" }, new User { Name = "User3" }, }; var result = list1.Except(list2); result.ForEach(u => Console.WriteLine(u.Name)); } }
SelectMany 集合降维 SelectMany 可以把多维集合降维,比如把二维的集合平铺成一个一维的集合。
举例:
1 2 3 4 5 6 7 var collection = new int [][] { new int [] {1 , 2 , 3 }, new int [] {4 , 5 , 6 }, };var result = collection.SelectMany(x => x);
再来举个更贴合实际应用的例子。
例如有如下实体类(一个部门有多个员工):
1 2 3 4 5 6 7 8 9 class Department { public Employee[] Employees { get; set; } }class Employee { public string Name { get; set; } }
此时,我们拥有一个这样的数据集合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var departments = new [] { new Department () { Employees = new [] { new Employee { Name = "Bob" }, new Employee { Name = "Jack" } } }, new Department () { Employees = new [] { new Employee { Name = "Jim" }, new Employee { Name = "John" } } } };
现在我们可以使用 SelectMany 把各部门的员工查询到一个结果集中:
1 2 3 4 5 6 var allEmployees = departments.SelectMany(x => x.Employees); foreach(var emp in allEmployees) { Console.WriteLine(emp.Name); }
SelectMany 迪卡尔积运算 SelectMany 不光适用于单个包含多维集合对象的降维,也适用于多个集合之前的两两相互操作,比如进行迪卡尔积运算。
比如我们有这样两个集合:
1 2 var list1 = new List <string> { "a1" , "a2" };var list2 = new List <string> { "b1" , "b2" , "b3" };
现在我们需要把它进行两两组合,使用普通的方法,我们需要用嵌套循环语句来实现:
1 2 3 4 5 var result = newList<string>(); foreach (var s1 in list1) foreach (var s2 in list2) result.Add($"{s1}{s2}" );
改用 SelectMany 实现:
1 2 var result = list1.SelectMany(x => list2.Select(y => $"{x}{y}" ));
具有挑战性的问题来了,如何对 N 个集合进行迪卡尔积运算呢,比如有这样的集合数据:
1 2 3 4 5 6 7 var arrList = new List <string[]> { new string [] { "a1" , "a2" }, new string [] { "b1" , "b2" , "b3" }, new string [] { "c1" }, };
如何对上面的 arrList 中的各个集合进行两两组合呢?
在电商业务尤其是零售业务中的产品组合促销中这种需求很常见。
下面是一个使用 SelectMany 的实现,需要用到递归:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class Program { static void Main (string[] args) { var arrList = new List <string[]> { new string [] { "a1" , "a2" }, new string [] { "b1" , "b2" , "b3" }, new string [] { "c1" }, }; var result = Recursion(arrList, 0 , new List <string>()); result.ForEach(x => Console.WriteLine(x)); } static List<string> Recursion (List<string[]> list, int start, List<string> result) { if (start >= list.Count) return result; if (result.Count == 0 ) result = list[start].ToList(); else result = result.SelectMany(x => list[start].Select(y => x + y)).ToList(); result = Recursion(list, start + 1 , result); return result; } }
类似这种集合的迪卡尔积运算操作,也可以用 LINQ to Object 来代替 SelectMany 实现:
1 2 3 result = result.SelectMany(x => list[start].Select(y => x + y)).ToList(); result = (from a in result from b in list[start] select a + b).ToList();
LINQ to Object 比扩展方法看上去易读性更好,但写起来扩展方法更方便。
Aggregate 聚合 Aggregate 扩展方法可以对一个集合依次执行类似累加器的操作,就像滚雪球一样把数据逐步聚集在一起。
比如实现从 1 加到 10,用 Aggregate 扩展方法就很方便:
1 2 3 int [] numbers = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 };int sum = numbers.Aggregate((prevSum, current) => prevSum + current);
我们来解析一下它的执行步骤
第一步,prevSum 取第一个元素的值,即 prevSum = 1
第二步,把第一步得到的 prevSum 的值加上第二个元素,即 prevSum = prevSum + 2
依此类推,第 i 步把第 i-1 得到的 prevSum 加上第 i 个元素
再来看一个字符串的例子加深理解:
1 2 3 string[] stringList = { "Hello" , "World" , "!" };string joinedString = stringList.Aggregate((prev, current) => prev + " " + current);
Aggregate 还有一个重载方法,可以指定累加器的初始值。我们来看一个比较综合的复杂例子。假如我们有如下 1-12 的一个数字集合:
1 var items = new List <int > { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 };
现在我们想做如下计算:
计算集合元素的总数个数
计算值为偶数的元素个数
收集每第 4 个元素
当然通过普通的循环遍历也可以实现这三个计算,但使用 Aggregate 会更简洁,下面是 Aggregate 的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var result = items.Aggregate(new { Total = 0 , Even = 0 , FourthItems = new List <int >() }, (accum, item) => new { Total = accum.Total + 1 , Even = accum.Even + (item % 2 == 0 ? 1 : 0 ), FourthItems = (accum.Total + 1 ) % 4 == 0 ? new List <int >(accum.FourthItems) { item } : accum.FourthItems } );
这里为了简单起见使用匿名类型作为累加器的初始值,由于匿名类型的属性是只读的,所以在累加的过程都 new 了一个新对象。如果初始值使用的是自定义类型,那累加时就不需 new 新对象了。
SkipWhile 和 TakeWhile 扩展方法 SkipWhile 和 TakeWhile 扩展方法,它与 Skip 和 Take 不同的是,它们的参数是具体的条件。SkipWhile 从起始位置开始忽略元素,直到匹配到符合条件的元素停止忽略,往后就是要查询的结果;TakeWhile 从起始位置开始读取符合条件的元素,一旦遇到不符合条件的就停止读取,即使后面还有符合条件的也不再读取。
示例:
SkipWhile:
1 2 3 int [] list = { 42 , 42 , 6 , 6 , 6 , 42 };var result = list.SkipWhile(i => i == 42 );
TakeWhile:
1 2 3 int [] list = { 1 , 10 , 40 , 50 , 44 , 70 , 4 };var result = list.TakeWhile(item => item < 50 ).ToList();
Zip 拉链 Zip 扩展方法操作的对象是两个集合,它就像拉链一样,根据位置将两个系列中的每个元素依次配对在一起。其接收的参数是一个 Func 实例,该 Func 实例允许我们成对在处理两个集合中的元素。
如果两个集合中的元素个数不相等,那么多出来的将会被忽略。
1 2 3 4 5 6 7 8 9 10 11 12 int [] numbers = { 3 , 5 , 7 }; string[] words = { "three" , "five" , "seven" , "ignored" }; IEnumerable<string> zip = numbers.Zip(words, (n, w) => n + "=" + w); foreach (string s in zip) { Console.WriteLine(s); }
OfType 和 Cast 类型过滤与转换 OfType 用于筛选集合中指定类型的元素,Cast 可以把集合转换为指定类型,但要求源类型必须可以隐式转换为目标类型。
假如有如下数据:
1 2 3 4 5 6 7 8 9 interface IFoo { }class Foo : IFoo { }class Bar : IFoo { }var item0 = new Foo ();var item1 = new Foo ();var item2 = new Bar ();var item3 = new Bar ();var collection = new IFoo [] { item0, item1, item2, item3 };
OfType 示例:
1 2 3 4 5 6 7 var foos = collection.OfType<Foo>(); var bars = collection.OfType<Bar>(); var foosAndBars = collection.OfType<IFoo>(); var foos = collection.Where(item => item is Foo); var bars = collection.Where(item => item is Bar);
Cast 示例:
1 2 3 var bars = collection.Cast<Bar>(); var foos = collection.Cast<Foo>(); var foosAndBars = collection.Cast<IFoo>();
ToLookup 索引式查找 ToLookup 扩展方法返回的是可索引查找的数据结构,它是一个 ILookup 实例,所有元素根据指定的键进行分组并可以按键进行索引。
这样说有点抽象,来看具体示例:
1 2 3 4 5 6 7 string[] array = { "one" , "two" , "three" };var lookup = array.ToLookup(item => item.Length);var result = lookup[3 ];
再来一个示例
1 2 3 4 5 6 7 8 9 10 11 int [] array = { 1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 };var lookup = array.ToLookup(item => item % 2 );var even = lookup[0 ];var odd = lookup[1 ];
ToDictionary 字典转换 ToDictionary 扩展方法可以把集合 IEnumerable 转换为 Dictionary<TKey, TValue> 结构的字典,它接收一个 Func<TSource, TKey> 参数用来返回每个元素指定的键与值。
示例:
1 2 IEnumerable<User> users = GetUsers(); Dictionary<int , User> usersById = users.ToDictionary(x => x.Id);
如果不用 ToDictionary,你需要这样写:
1 2 3 4 5 6 IEnumerable<User> users = GetUsers(); Dictionary<int , User> usersById = new Dictionary <int , User>(); foreach (User u in users) { usersById.Add(u.Id, u); }
上面 ToDictionary 返回的字典数据中的值是整个元素,你也可以通过它的第二个参数来自定义字典的值。示例:
1 Dictionary<int , string> userNamesById = users.ToDictionary(x => x.Id, x => x.Name);
你也可以为转换的字典指定其键是否区分大小写,即自定义字典的 IComparer,示例:
1 2 3 4 5 6 Dictionary<string, User> usersByCaseInsenstiveName = users.ToDictionary(x =>x.Name, StringComparer.InvariantCultureIgnoreCase);var user1 = usersByCaseInsenstiveName["liam" ];var user2 = usersByCaseInsenstiveName["LIAM" ]; user1 == user2;
注意,字典类型要求所有键不能重复,所以在使用 ToDictionary 方法时要确保作为字典的键的元素属性不能有重复值,否则会抛出异常。
其它常见扩展方法 LINQ 还有很多其它常见的扩展方法,大家在平时应该用的比较多,比如 Where、Any、All 等,这里也选几个简单举例介绍一下。
Range 和 Repeat Range 和 Repeat 用于生成简单的数字或字符串系列。示例:
1 2 3 4 5 var range = Enumerable.Range(1 , 100 );var repeatedValues = Enumerable.Repeat("a" , 3 );
Any 和 All Any 用来判断集合中是否存在任一一个元素符合条件,All 用来判断集合中是否所有元素符合条件。示例:
1 2 3 4 5 var numbers = new int [] {1 , 2 , 3 , 4 , 5 };bool result = numbers.Any(); bool result = numbers.Any(x => x == 6 ); bool result = numbers.All(x => x > 0 ); bool result = numbers.All(x => x > 1 );
Concat 和 Union Concat 用来拼接两个集合,不会去除重复元素,示例:
1 2 3 4 5 6 List<int > foo = newList<int > { 1 , 2 , 3 }; List<int > bar = newList<int > { 3 , 4 , 5 };var result = Enumerable.Concat(foo, bar).ToList(); var result = foo.Concat(bar).ToList();
Union 也是用来拼接两个集合,与 Concat 不同的是,它会去除重复项,示例:
1 var result = foo.Union(bar);
GroupBy 分组 GroupBy 扩展方法用来对集合进行分组,下面是一个根据奇偶进行分组的示例:
1 2 3 var list = new List <int >() { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 };var grouped = list.GroupBy(x => x % 2 == 0 );
还可以根据指定属性进行分组:
1 2 3 4 5 6 7 8 9 10 public class Person { public int Age { get; set; } public string Name { get; set; } }var people = new List <Person>();var query = people .GroupBy(x => x.Age) .Select(g => { Age = g.Key, Count = g.Count() });
SequenceEqual 集合相等 SequenceEqual 扩展方法用于比较集合系列各个相同位置的元素是否相等。示例:
1 2 3 4 5 6 int [] a = new int [] {1 , 2 , 3 };int [] b = new int [] {1 , 2 , 3 };int [] c = new int [] {1 , 3 , 2 };bool result1 = a.SequenceEqual(b); bool result2 = a.SequenceEqual(c);