LINQ 第 2 部分:标准方法 - 工具箱中的工具






4.78/5 (15投票s)
快速浏览System.Linq.Enumerable类中定义的所有标准LINQ方法
引言
在本系列的第一部分中,我们深入研究了IEnumerable
并开始介绍一些标准LINQ方法。这些方法都定义在System.Linq.Enumerable
类中,大多作为IEnumerable
的扩展方法。
如果您对扩展方法或IEnumerable
不清楚,请回顾上一篇文章
本文大量使用了 Lambda 表达式和匿名类型。如果您不熟悉这些概念,本文末尾将对它们进行简要介绍。
背景
在本文中,我们将快速浏览标准LINQ方法。我们将为每种方法提供示例代码和输出。本文仅旨在向您介绍这些方法。它**不**旨在让您成为其使用专家。
为了方便快速浏览,示例的长度特意控制到最短。它们只使用了这些方法的最简单的重载。您可以将本文视为一份可用食材清单。关于如何使用这些食材烘焙蛋糕的说明将在后续文章中提供。
阅读本文的最佳方法是快速浏览一遍。不必担心记住细节。您可以随时参考本文或相关的Microsoft文档。本文末尾包含指向这些文档的链接。
这是LINQ系列文章中的第二篇。指向本系列其他文章的链接如下
- LINQ 第 1 部分:深入了解 IEnumerable
- LINQ 第 2 部分:标准方法 - 工具箱中的工具
- LINQ 第 3 部分:IQueryable 简介
- LINQ 第 4 部分:深入了解 Queryable 扩展方法
新建序列
System.Linq.Enumerable
中的一些方法会创建全新的序列。这些方法包括:Empty
、Range
和Repeat
。
Empty
Empty
方法创建一个空序列(不包含任何项)。
WriteSequence("Empty", Enumerable.Empty<int>());
// Empty:
Range
Range
方法创建一个包含整数值范围的序列。
WriteSequence("Range", Enumerable.Range(1, 5));
// Range: 1, 2, 3, 4, 5
Repeat
Repeat
方法创建一个包含单个项重复出现的序列。
WriteSequence("Repeat", Enumerable.Repeat("ABC", 3));
// Repeat: ABC, ABC, ABC
按原始顺序的序列
一些方法会返回一个新序列,其中包含原始序列的全部或部分内容,并保持原始顺序。这些方法在开始返回项之前不需要具体化序列。这些方法包括:Append
、AsEnumerable
、Cast
、Concat
、DefaultIfEmpty
、OfType
、Prepend
、Range
、Repeat
、Select
、SelectMany
、Skip
、SkipWhile
、Take
、TakeWhile
、Where
和Zip
。
由于它们最容易描述,我们将首先介绍它们
Append
Append
方法在序列末尾添加单个项。
string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence(berries.Append("strawberry"));
// Append: blackberry, blueberry, raspberry, strawberry
AsEnumerable
AsEnumerable
方法超出了本文的范围。它允许从其他LINQ口味(IQueryable
)过渡到本文描述的口味(IEnumerable
)。我们将在后续文章中重新讨论此方法并进行更详细的解释。
string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence("AsEnumerable", berries.AsEnumerable());
// AsEnumerable: blackberry, blueberry, raspberry
Cast
有时,需要处理弱类型序列,尤其是在处理旧版.NET数据类型时。在这些情况下,序列可能实现了IEnumerable
,但没有实现IEnumerable<T>
。当序列中的所有数据类型已知为相同数据类型时,Cast
方法允许您将所有项转换为该数据类型。
Cast
方法将序列中的每个项转换为不同的数据类型。
var intList = new ArrayList { 123, 456, 789 };
IEnumerable<int> ints = intList.Cast<int>();
WriteSequence("Cast", ints);
// Cast: 123, 456, 789
如果任何项的数据类型与指定的不同,则会发生异常。在这种情况下,使用OfType
方法会更好。
var list = new ArrayList { "Alpha", 123, "Beta", "Gamma" };
WriteSequence("Cast", list.Cast<string>());
// Unhandled Exception: System.InvalidCastException: Specified cast is not valid.
Concat
Concat
方法将另一个序列的项添加到序列的末尾。
string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence("Concat", berries.Concat(citrus));
// Concat: blackberry, blueberry, raspberry, grapefruit, lemon, lime, orange
DefaultIfEmpty
DefaultIfEmpty
方法返回原始序列,或者在原始序列为空(不包含任何项)时返回一个包含单个默认值的序列。
byte[] bytes = { 1, 2, 3, 4 };
WriteSequence("DefaultIfEmpty: ", bytes.DefaultIfEmpty());
// DefaultIfEmpty: 1, 2, 3, 4
byte[] emptyBytes = { };
WriteSequence("DefaultIfEmpty: ", emptyBytes.DefaultIfEmpty());
// DefaultIfEmpty: 0
WriteSequence("DefaultIfEmpty: ", emptyBytes.DefaultIfEmpty((byte)123));
// DefaultIfEmpty: 123
OfType
有时,需要处理弱类型序列,尤其是在处理旧版.NET数据类型时。在这些情况下,序列可能实现了IEnumerable
,但没有实现IEnumerable<T>
。OfType
方法允许您从该序列中选择项(同质类型),并生成一个IEnumerable<T>
实例。
var list = new ArrayList { "Alpha", 123, "Beta", "Gamma" };
WriteSequence("OfType", list.OfType<string>());
// OfType: Alpha, Beta, Gamma
Prepend
Prepend
方法在序列开头添加单个项。
string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence("Prepend", berries.Prepend("strawberry"));
// Prepend: strawberry, blackberry, blueberry, raspberry
Select
Select
方法修改序列中的每个项。
string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence("Select", berries.Select(berry => berry + " more"));
// Select: blackberry more, blueberry more, raspberry more
SelectMany
SelectMany
方法修改多个序列并将它们展平成一个单一序列。
int[][] manyNumbers = { new int[] { 1, 2 }, new int[] { 3, 4 }, new int[] { 5, 6 } };
WriteSequence("SelectMany", manyNumbers.SelectMany(numbers => numbers.Append(9)));
// SelectMany: 1, 2, 9, 3, 4, 9, 5, 6, 9
Skip
Skip
方法跳过序列开头固定数量的项。
char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("Skip", letters.Skip(1));
// Skip: B, C, D
SkipWhile
SkipWhile
方法在序列开头跳过满足指定条件的项。
char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("SkipWhile", letters.SkipWhile(letter => letter < 'C'));
// SkipWhile: C, D
Take
Take
方法从序列开头选取固定数量的项。
char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("Take", letters.Take(1));
// Take: A
TakeWhile
TakeWhile
方法从序列开头选取满足指定条件的项。
char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("TakeWhile", letters.TakeWhile(letter => letter < 'C'));
// TakeWhile: A, B
其中
Where
方法选取所有满足指定条件的项。
string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
WriteSequence("Where", citrus.Where(name => name.Contains("a")));
// Where: grapefruit, orange
Zip
Zip
方法将一个序列与另一个序列的项组合起来。
string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("Zip", citrus.Zip(letters, (fruit, letter) => $"{fruit} {letter}"));
// Zip: grapefruit A, lemon B, lime C, orange D
新顺序的序列
一些方法会返回一个新序列,其中包含原始序列的全部或部分内容,但顺序不同。虽然执行仍然被延迟到您开始消耗序列时,但这可能会产生误导。为了返回序列中的第一个项,这些方法必须首先评估(并具体化)部分或全部序列。这些方法包括:Distinct
、Except
、GroupBy
、GroupJoin
、Intersect
、Join
、OrderBy
、OrderByDescending
、Reverse
、ThenBy
、ThenByDescending
和Union
。
Distinct
Distinct
方法从序列中选择唯一的项。当同一项在序列中出现多次时,只包含该项的一个实例。
string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("Distinct", stooges.Distinct());
// Distinct: Moe Howard, Larry Fine, Curly Howard
Except
Except
方法获取序列中存在而另一个序列中不存在的唯一项。
string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
string[] fruits = { "banana", "banana", "lemon", "lime", "lime" };
WriteSequence("Except: ", fruits.Except(citrus));
// Except: Banana
GroupBy
GroupBy
方法根据唯一的特征(或键)对序列中的项进行分组。它生成一个分组序列,其中每个分组本身就是一个序列,并带有一个关联的键值。
string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("GroupBy: ", stooges.GroupBy(name => GetLastWord(name)));
// GroupBy: [Key=Howard, Items=Moe Howard, Curly Howard, Moe Howard], [Key=Fine, Items=Larry Fine]
GetLastWord
方法的代码如下
private static string GetLastWord(string words) =>
words.Substring(words.LastIndexOf(' ') + 1);
GroupJoin
GroupJoin
方法类似于SQL的LEFT OUTER JOIN
,演示起来异常困难。因此,其完整描述将在本文后面“GroupJoin和Join”标题下提供。
var courseFaculty = courses.GroupJoin(faculty, course => course.Id, teacher => teacher.CourseId,
(course, teachers) => new { Course = course.Name, Teachers = teachers });
Intersect
Intersect
方法获取两个序列共有的唯一项。
string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
string[] fruits = { "banana", "banana", "lemon", "lime", "lime" };
WriteSequence("Intersect: ", citrus.Intersect(fruits));
// Intersect: lemon, lime
Join
Join
方法类似于SQL的LEFT INNER JOIN
,演示起来异常困难。因此,其完整描述将在本文后面“GroupJoin和Join”标题下提供。
var courseTeacher = courses.Join(faculty, course => course.Id, teacher => teacher.CourseId,
(course, teacher) => new { Course = course.Name, Teacher = teacher.Name });
OrderBy
OrderBy
方法根据从序列中每个项获取的键值,按升序对序列进行排序。
string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("OrderBy: ", stooges.OrderBy(name => GetFirstWord(name)));
// OrderBy: Curly Howard, Larry Fine, Moe Howard, Moe Howard
GetFirstWord
方法的代码如下
private static string GetFirstWord(string words) =>
words.Substring(0, words.IndexOf(' '));
OrderByDescending
OrderByDescending
方法根据从序列中每个项获取的键值,按降序对序列进行排序。
string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("OrderByDescending: ", stooges.OrderByDescending(name => GetFirstWord(name)));
// OrderByDescending: Moe Howard, Moe Howard, Larry Fine, Curly Howard
GetFirstWord
方法的代码如下
private static string GetFirstWord(string words) =>
words.Substring(0, words.IndexOf(' '));
Reverse
Reverse
方法反转序列的顺序。
string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("Reverse: ", stooges.Reverse());
// Reverse: Moe Howard, Curly Howard, Larry Fine, Moe Howard
ThenBy
ThenBy
方法有些独特,因为它只能应用于IOrderedEnumerable<T>
,通常是从以下方法之一返回的:OrderBy
、OrderByDescending
、ThenBy
或ThenByDescending
。此方法提供了按序列中每个项的附加键值按升序对序列进行排序的功能。
var sortedStooges = stooges
.OrderBy(name => GetLastWord(name))
.ThenBy(name => GetFirstWord(name));
WriteSequence("ThenBy: ", sortedStooges);
// ThenBy: Larry Fine, Curly Howard, Moe Howard, Moe Howard
ThenByDescending
ThenByDescending
方法有些独特,因为它只能应用于IOrderedEnumerable<T>
,通常是从以下方法之一返回的:OrderBy
、OrderByDescending
、ThenBy
或ThenByDescending
。此方法提供了按序列中每个项的附加键值按降序对序列进行排序的功能。
var sortedStooges = stooges
.OrderBy(name => GetLastWord(name))
.ThenByDescending(name => GetFirstWord(name));
WriteSequence("ThenByDescending: ", sortedStooges);
// ThenByDescending: Larry Fine, Moe Howard, Moe Howard, Curly Howard
Union
Union
方法获取两个序列组合后产生的唯一项。
string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
string[] fruits = { "banana", "banana", "lemon", "lime", "lime" };
WriteSequence("Union: ", citrus.Union(fruits));
// Union: grapefruit, lemon, lime, orange, banana
单例方法
单例方法强制立即具体化至少一部分序列。这些方法包括:Aggregate
、All
、Any
、Average
、Contains
、Count
、ElementAt
、ElementAtOrDefault
、First
、FirstOrDefault
、Last
、LastOrDefault
、LongCount
、Max
、Min
、SequenceEqual
、Single
、SingleOrDefault
、Sum
、ToArray
、ToDictionary
、ToList
和ToLookup
。
Aggregate
Aggregate
方法累加序列中的项以返回一个单例聚合值。下面的代码仅为示例。它是String.Join
的一个糟糕的实现,如果您只需要简单地聚合字符串,String.Join
会更合适。
string[] berries = { "blackberry", "blueberry", "raspberry" };
string aggregate = berries.Aggregate((accumulator, item) => accumulator + " " + item);
Console.WriteLine($"Aggregate: {aggregate}");
// Aggregate: blackberry blueberry raspberry
警告:如果序列包含零项,将发生异常。如果序列只包含一项,则直接返回第一项,而不会调用您提供的方法。对于所有其他序列,将使用序列的前两项调用您方法的第一次调用。
全部
All
方法获取一个布尔值,指示序列中的所有项是否都匹配指定条件。当遇到第一个不匹配指定条件的项时,此方法停止。
char[] letters = { 'A', 'B', 'C', 'D'};
bool all = letters.All(item => Char.IsLetter(item));
Console.WriteLine($"All: {all}");
// All: True
任意
Any
方法获取一个布尔值,指示序列中的任何项是否匹配指定条件。当遇到第一个匹配指定条件的项时,此方法停止。
char[] letters = { 'A', 'B', 'C', 'D'};
bool any = letters.Any(item => Char.IsLetter(item));
Console.WriteLine($"Any: {any}");
// Any: True
Average
Average
方法计算序列中所有项的平均值并返回浮点结果。
byte[] bytes = { 1, 2, 3, 4 };
float average = bytes.Average(item => (float)item);
Console.WriteLine($"Average: {average}");
// Average: 2.5
Contains
Contains
方法获取一个布尔值,指示序列是否包含指定项。当遇到第一个与指定项匹配的项时,此方法停止。
char[] letters = { 'A', 'B', 'C', 'D'};
bool contains = letters.Contains('C');
Console.WriteLine($"Contains {contains}");
// Contains: True
Count
Count
方法获取序列中的项数。
// The Length property would be more efficient here
byte[] bytes = { 1, 2, 3, 4 };
int count = bytes.Count();
Console.WriteLine($"Count: {count}");
// Count: 4
Count
方法还可以获取序列中与指定条件匹配的项数。
count = bytes.Count(item => (item & 1) == 0);
Console.WriteLine($"Count: {count}");
// Count: 2
警告:IEnumerable<T>.Count
方法必须评估序列中的每个项,可能会消耗大量资源。不应将其与ICollection<T>.Count
属性或Array.Length
属性混淆。这两者(大多数情况下)仅返回预先存在的计数,并且(通常)不消耗大量资源。
ElementAt
ElementAt
方法获取序列中指定零基索引处的项。
byte[] bytes = { 1, 2, 3, 4 };
byte elementAt = bytes.ElementAt(1);
Console.WriteLine($"ElementAt: {elementAt}");
// ElementAt: 2
elementAt = bytes.ElementAt(10);
// Unhandled Exception: System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
警告:此方法不应与数组和列表提供的索引功能混淆。与真正的索引不同,ElementAt
方法必须评估并跳过指定索引之前的所有项。其功能等同于以下LINQ:IEnumerable<T>.Skip(index).First()
。
ElementAtOrDefault
ElementAtOrDefault
方法获取序列中指定零基索引处的项,如果指定项不存在,则返回默认值。
byte elementAtOrDefault = bytes.ElementAt(1);
Console.WriteLine($"ElementAtOrDefault: {elementAtOrDefault}");
// ElementAt: 2
elementAtOrDefault = bytes.ElementAtOrDefault(-1);
Console.WriteLine($"ElementAtOrDefault: {elementAtOrDefault}");
// ElementAt: 0
警告:此方法不应与数组和列表提供的索引功能混淆。与真正的索引不同,ElementAtOrDefault
方法必须评估并跳过指定索引之前的所有项。其功能等同于以下LINQ:IEnumerable<T>.Skip(index).FirstOrDefault()
。
First
First
方法获取序列中的第一项或匹配指定条件的第一项。如果允许序列为空,则首选FirstOrDefault
方法。如果序列不允许超过一项,则首选Single/SingleOrDefault
方法。
string[] berries = { "blackberry", "blueberry", "raspberry" };
string first = berries.First();
Console.WriteLine("First: {first}");
// First: blackberry
first = berries.First(item => item.StartsWith("r"));
Console.WriteLine($"First: {first}");
// First: raspberry
byte[] emptyBytes = { };
byte firstEmpty = emptyBytes.First();
// Unhandled Exception: System.InvalidOperationException: Sequence contains no elements
FirstOrDefault
FirstOrDefault
方法获取序列中的第一项或匹配指定条件的第一项。如果序列为空,它将返回默认值。如果序列不允许超过一项,则首选SingleOrDefault
方法。
string[] berries = { "blackberry", "blueberry", "raspberry" };
string firstOrDefault = berries.FirstOrDefault();
Console.WriteLine($"FirstOrDefault: {firstOrDefault}");
// FirstOrDefault: blackberry
firstOrDefault = berries.FirstOrDefault(item => item.StartsWith("r"));
Console.WriteLine($"FirstOrDefault: {firstOrDefault}");
// FirstOrDefault: raspberry
byte[] emptyBytes = { };
byte firstEmpty = emptyBytes.FirstOrDefault();
Console.WriteLine($"FirstOrDefault: {firstEmpty}");
// FirstOrDefault: 0
Last
Last
方法获取序列中的最后一项。如果允许序列为空,则首选LastOrDefault
方法。
string last = berries.Last();
Console.WriteLine($"Last: {last}");
// Last: raspberry
last = berries.Last(item => item.StartsWith("b"));
Console.WriteLine($"Last: {last}");
// Last: blueberry
byte[] emptyBytes = { };
byte lastEmpty = emptyBytes.LastOrDefault();
Console.WriteLine($"LastOrDefault: {lastEmpty}");
// Unhandled Exception: System.InvalidOperationException: Sequence contains no elements
警告:与First
方法不同,Last
方法必须访问序列中的每一项。对于较大的序列,这可能会消耗大量资源。
LastOrDefault
LastOrDefault
方法获取序列中的最后一项或匹配指定条件的最后一项。如果序列为空,它将返回默认值。
string lastOrDefault = berries.LastOrDefault();
Console.WriteLine($"LastOrDefault: {lastOrDefault}");
// LastOrDefault: raspberry
lastOrDefault = berries.LastOrDefault(item => item.StartsWith("b"));
Console.WriteLine($"LastOrDefault: {lastOrDefault}");
// LastOrDefault: blueberry
byte[] emptyBytes = { };
byte lastEmpty = emptyBytes.LastOrDefault();
Console.WriteLine($"LastOrDefault: {lastEmpty}");
// LastOrDefault: 0
警告:与FirstOrDefault
方法不同,LastOrDefault
必须访问序列中的每一项。对于较大的序列,这可能会消耗大量资源。
LongCount
LongCount
方法获取序列中的项数(作为64位计数)。
// The Length property would be more efficient here
byte[] bytes = { 1, 2, 3, 4 };
long count = bytes.LongCount();
Console.WriteLine($"LongCount: {count}");
// Count: 4
LongCount
方法还可以获取序列中与指定条件匹配的项数(作为64位计数)。
count = bytes.LongCount(item => (item & 1) == 0);
Console.WriteLine($"LongCount: {count}");
// Count: 2
警告:IEnumerable<T>.LongCount
方法必须评估序列中的每个项,可能会消耗大量资源。不应将其与ICollection<T>.Count
属性或Array.Length
属性混淆。这两者(大多数情况下)仅返回预先存在的计数,并且(通常)不消耗大量资源。
最大值
Max
方法获取序列中的最大值。
byte[] bytes = { 1, 2, 3, 4 };
byte max = bytes.Max();
Console.WriteLine($"Max: {max}");
// Max: 4
string[] textNumbers = { "1", "2", "3", "4" };
max = textNumbers.Max(item => byte.Parse(item));
Console.WriteLine($"Max: {max}");
// Max: 4
最小值
Min
方法获取序列中的最小值。
byte[] bytes = { 1, 2, 3, 4 };
byte min = bytes.Min();
Console.WriteLine($"Min: {min}");
// Min: 1
string[] textNumbers = { "1", "2", "3", "4" };
min = textNumbers.Min(item => byte.Parse(item));
Console.WriteLine($"Min: {min}");
// Min: 1
SequenceEqual
SequenceEquals
方法获取一个布尔值,指示两个序列是否具有相等数量且相等且顺序相同的项。
byte[] bytes = { 1, 2, 3, 4 };
bool sequenceEqual = bytes.SequenceEqual(bytes.Take(bytes.Length));
Console.WriteLine($"SequenceEqual: {sequenceEqual}");
// SequenceEqual: True
sequenceEqual = bytes.SequenceEqual(bytes.Take(bytes.Length - 1));
Console.WriteLine($"SequenceEqual: {sequenceEqual}");
// SequenceEqual: False
Single
Single
方法获取序列中的第一项或匹配指定条件的第一项。如果没有完全匹配的项,此方法将抛出异常。如果允许序列为空,则首选SingleOrDefault
方法。
string[] berry = { "blackberry" };
string single = berry.Single();
Console.WriteLine($"Single: {single}");
// Single: blackberry
byte singleEmpty = emptyBytes.Single();
// Unhandled Exception: System.InvalidOperationException: Sequence contains no elements
string[] berries = { "blackberry", "blueberry", "raspberry" };
single = berries.Single();
// Unhandled Exception: System.InvalidOperationException: Sequence contains more than one element
SingleOrDefault
SingleOrDefault
方法获取序列中的第一项,如果存在多于一项,则抛出异常,如果序列为空,则返回默认值。
string[] berry = { "blackberry" };
string singleOrDefault = berry.SingleOrDefault();
Console.WriteLine($"SingleOrDefault: {singleOrDefault}");
// Single: blackberry
byte singleEmpty = emptyBytes.SingleOrDefault();
Console.WriteLine($"SingleOrDefault: {singleEmpty}");
// SingleOrDefault: 0
string[] berries = { "blackberry", "blueberry", "raspberry" };
string singleOrDefault = berries.SingleOrDefault();
// Unhandled Exception: System.InvalidOperationException: Sequence contains more than one element
Sum
Sum
方法计算序列中所有值的总和。
byte[] bytes = { 1, 2, 3, 4 };
float sum = bytes.Sum(item => (float)item);
Console.WriteLine($"Sum: {sum}");
// Sum: 10
ToArray
ToArray
方法将序列转换为TItem[]
类型的数组。
byte[] bytes = { 1, 2, 3, 4 };
byte[] byteArray = bytes
.Take(2)
.ToArray();
警告:除非绝对必要,否则应避免使用此方法。执行此方法会导致序列中的所有项都被评估。它还要求序列的所有成员同时存在于内存中。对于较大的序列,这可能会消耗大量资源。
ToDictionary
ToDictionary
方法将序列转换为System.Collections.Generic.Dictionary<TKey, TITem>
。
string[] numberFruits = { "1 Apple", "2 Orange", "3 Cherry" };
Dictionary<int, string> fruitDictionary = numberFruits.ToDictionary(
item => int.Parse(GetFirstWord(item)), item => GetLastWord(item));
警告:除非绝对必要,否则应避免使用此方法。执行此方法会导致序列中的所有项都被评估。它还要求序列的所有成员同时存在于内存中。对于较大的序列,这可能会消耗大量资源。
ToList
ToList
方法将序列转换为System.Collections.Generic.List<TItem>
。
byte[] bytes = { 1, 2, 3, 4 };
List<byte> byteList = bytes.ToList();
警告:除非绝对必要,否则应避免使用此方法。执行此方法会导致序列中的所有项都被评估。它还要求序列的所有成员同时存在于内存中。对于较大的序列,这可能会消耗大量资源。
ToLookup
ToLookup
方法将序列转换为System.Linq.Lookup<TKey, TItem>
实例。
ILookup<int, string> fruitLookup = numberFruits.ToLookup(
item => int.Parse(GetFirstWord(item)));
警告:除非绝对必要,否则应避免使用此方法。执行此方法会导致序列中的所有项都被评估。它还要求序列的所有成员同时存在于内存中。对于较大的序列,这可能会消耗大量资源。
GroupJoin和Join
GroupJoin和Join方法都非常复杂。在这里,我们将对它们各自进行简要描述。在这两种情况下,我们将使用以下数据
var courses = new[]
{
new { Id = 1, Name = "Geometry" },
new { Id = 2, Name = "Physics" },
new { Id = 3, Name = "Parapsychology" },
};
var faculty = new[]
{
new { CourseId = 1, Name = "Pythagoras" },
new { CourseId = 1, Name = "Euclid" },
new { CourseId = 2, Name = "Albert Einstein" },
new { CourseId = 2, Name = "Isaac Newton"}
};
GroupJoin
GroupJoin方法有点难以描述。它将一个序列中的所有项与第二个序列中的匹配项连接起来。它还对这些匹配项进行分组。对于熟悉SQL的人来说,它类似于(但又令人沮丧地不同于)LEFT OUTER JOIN
。最简单的重载接受以下参数
- 要连接的序列。
- 一个从原始序列中的项获取连接键的方法。
- 一个从要连接的序列中的项获取连接键的方法。
- 一个将原始序列中的项与要连接的序列中的匹配项(或在不存在匹配时为null)连接起来的方法。
var courseFaculty = courses.GroupJoin(faculty, course => course.Id, teacher => teacher.CourseId,
(course, teachers) => new { Course = course.Name, Teachers = teachers });
Console.Write("GroupBy: ");
int index = 0;
foreach (var courseTeachers in courseFaculty)
{
if (index++ >= 1)
Console.Write(", ");
WriteSequence($"[Course={courseTeachers.Course}, Teachers=", courseTeachers.Teachers, false);
Console.Write("]");
}
Console.WriteLine();
// GroupBy: [Course = Geometry, Teachers ={ CourseId = 1, Name = Pythagoras }, { CourseId = 1, Name = Euclid }], [Course=Physics, Teachers={ CourseId = 2, Name = Albert Einstein }, { CourseId = 2, Name = Isaac Newton }], [Course=Parapsychology, Teachers=]
由于GroupJoin的结果有点难以处理,处理此问题的一种常见技术是“展平”结果,使其更接近LEFT OUTER JOIN
。
var flattened = courseFaculty.SelectMany(
courseTeachers => courseTeachers.Teachers.DefaultIfEmpty(),
(courseTeachers, teacher) => new { courseTeachers.Course, Teacher = teacher?.Name });
WriteSequence("GroupBy/SelectMany: ", flattened);
// GroupBy / SelectMany: { Course = Geometry, Teacher = Pythagoras }, { Course = Geometry, Teacher = Euclid }, { Course = Physics, Teacher = Albert Einstein }, { Course = Physics, Teacher = Isaac Newton }, { Course = Parapsychology, Teacher = }
Join
Join方法连接来自两个不同序列的匹配项。对于熟悉SQL的人来说,它几乎与LEFT INNER JOIN
相同。最简单的重载接受以下参数
- 要连接的序列。
- 一个从原始序列中的项获取连接键的方法。
- 一个从要连接的序列中的项获取连接键的方法。
- 一个将原始序列中的项与要连接的序列中的匹配项连接起来的方法。
var courseTeacher = courses.Join(faculty, course => course.Id, teacher => teacher.CourseId,
(course, teacher) => new { Course = course.Name, Teacher = teacher.Name });
WriteSequence("Join: ", courseTeacher);
// Join: { Course = Geometry, Teacher = Pythagoras }, { Course = Geometry, Teacher = Euclid }, { Course = Physics, Teacher = Albert Einstein }, { Course = Physics, Teacher = Isaac Newton }
匿名类型
匿名类型是在.NET 3.0中引入的,以配合LINQ。它们提供了一种语法捷径,可以在一步中声明和初始化类型。使用匿名类型,可以实现以下功能
var myAnonymousType = new { Id = 1, Name = "Me" };
Console.WriteLine(myAnonymousType.Name);
// { Id = 1, Name = Me }
在.NET 3.0之前,要实现相同的目标,我们首先需要定义类型
private class NotSoAnonymousName
{
private string name;
private int id;
public int Id { get { return id; } set { id = value; } }
public string Name { get { return name; } set { name = value; } }
public override string ToString()
{
return string.Format("{{ Id = {0}, Name = {1} }}", Id, Name);
}
}
然后按名称引用类型
NotSoAnonymousName notSoAnonymousType = new NotSoAnonymousName { Id = 1, Name = "Me" };
Console.WriteLine(myNotSoAnonymousType);
// { Id = 1, Name = Me }
类声明特别冗长,因为当时还无法使用自动实现的属性、表达式体成员和字符串插值。供参考,如果它们可用,类声明至少可以缩短为
private class NotSoAnonymousName
{
public int Id { get; set; }
public string Name { get; set; } public override string ToString() =>
$"{{ Id = {Id}, Name = {Name} }}";
}
Lambda 表达式
Lambda表达式是在.NET 3.0中引入的,以配合LINQ。它们提供了一种语法捷径,用于创建和共享匿名方法声明。使用Lambda表达式,可以实现以下功能
string[] berries = { "blackberry", "blueberry", "raspberry" };
string firstBerry = berries.First(item => item.StartsWith("r"));
Console.WriteLine($"First: {firstBerry}");
// First: blackberry
Lambda表达式由参数(左侧)和方法逻辑(右侧)组成,用“=>”分隔
item => item.StartsWith("r")
在Lambda表达式之前,要实现相同的目标,我们将被迫声明一个方法
private static bool StartswithAnR(string item)
{
return item.StartsWith("r");
}
并将其委托作为参数传递
firstBerry = berries.First(StartswithAnR);
Console.WriteLine($"First: {firstBerry}");
// First: blackberry
使用前面的Lambda方法,我们实际上是在声明一个接受一个参数(item)并返回一个值的匿名方法。返回值的类型(string)由item的使用方式推断。返回值的类型(bool)也类似地被推断。
C#还提供了许多预制的委托。这使我们能够为参数和变量声明提供类型安全的方法委托。其中之一是Func委托,其最后一个类型参数(此处为bool)是返回值类型,所有前面的类型参数(此处为string)表示函数/方法期望的参数类型。
使用与之前相同的Lambda表达式,我们可以将其分配给委托,然后使用该委托
Func<string, bool> predicate = item => item.StartsWith("r");
bool startsWithAnR = predicate("right");
Console.WriteLine($"startsWithAnR = {startsWithAnR}");
// startsWithAnR = True Below is an example of a Lambda expression with multiple parameters.
Func<string, string, string> concatenate = (textA, textB) => textA + textB;
string result = concatenate("left ", "right");
Console.WriteLine($"result = {result}");
// result = left right
WriteSequence 方法
本文通篇使用了以下方法。
private static void WriteSequence<TItem>(string prefix, IEnumerable<TItem> sequence,
bool writeLine = true)
{
Console.Write(prefix);
int index = 0;
foreach (TItem item in sequence)
{
if (index++ >= 1)
Console.Write(", ");
Console.Write(item);
}
if (writeLine)
Console.WriteLine();
}
private static void WriteSequence<TKey, TItem>(string prefix,
IEnumerable<IGrouping<TKey, TItem>> sequence)
{
Console.Write(prefix);
int index = 0;
foreach (IGrouping<TKey, TItem> grouping in sequence)
{
if (index++ >= 1)
Console.Write(", ");
WriteSequence($"[Key={grouping.Key}, Items=", grouping, false);
Console.Write("]");
}
Console.WriteLine();
}
延伸阅读
更多阅读,请参阅以下内容
标准查询运算符概述 (C#)
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/standard-query-operators-overview
匿名类型 (C# 编程指南)
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/anonymous-types
Lambda 表达式 (C# 编程指南)
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions
扩展方法 (C# 编程指南)
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods
字符串插值 (C# 参考)
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/interpolated
历史
- 2018年4月18日 - 上传原始版本
- 2018年4月18日 - 修正了Join注释中的错误
- 2018年4月21日 - 添加了本系列其他文章的链接
- 2018年4月25日 - 添加了本系列第四篇文章的链接