65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (15投票s)

2018年4月18日

CPOL

15分钟阅读

viewsIcon

29049

downloadIcon

324

快速浏览System.Linq.Enumerable类中定义的所有标准LINQ方法

引言

在本系列的第一部分中,我们深入研究了IEnumerable并开始介绍一些标准LINQ方法。这些方法都定义在System.Linq.Enumerable类中,大多作为IEnumerable的扩展方法。

如果您对扩展方法或IEnumerable不清楚,请回顾上一篇文章

本文大量使用了 Lambda 表达式和匿名类型。如果您不熟悉这些概念,本文末尾将对它们进行简要介绍。

背景

在本文中,我们将快速浏览标准LINQ方法。我们将为每种方法提供示例代码和输出。本文仅旨在向您介绍这些方法。它**不**旨在让您成为其使用专家。

为了方便快速浏览,示例的长度特意控制到最短。它们只使用了这些方法的最简单的重载。您可以将本文视为一份可用食材清单。关于如何使用这些食材烘焙蛋糕的说明将在后续文章中提供。

阅读本文的最佳方法是快速浏览一遍。不必担心记住细节。您可以随时参考本文或相关的Microsoft文档。本文末尾包含指向这些文档的链接。

这是LINQ系列文章中的第二篇。指向本系列其他文章的链接如下

新建序列

System.Linq.Enumerable中的一些方法会创建全新的序列。这些方法包括:EmptyRangeRepeat

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

按原始顺序的序列

一些方法会返回一个新序列,其中包含原始序列的全部或部分内容,并保持原始顺序。这些方法在开始返回项之前不需要具体化序列。这些方法包括:AppendAsEnumerableCastConcatDefaultIfEmptyOfTypePrependRangeRepeatSelectSelectManySkipSkipWhileTakeTakeWhileWhereZip

由于它们最容易描述,我们将首先介绍它们

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

新顺序的序列

一些方法会返回一个新序列,其中包含原始序列的全部或部分内容,但顺序不同。虽然执行仍然被延迟到您开始消耗序列时,但这可能会产生误导。为了返回序列中的第一个项,这些方法必须首先评估(并具体化)部分或全部序列。这些方法包括:DistinctExceptGroupByGroupJoinIntersectJoinOrderByOrderByDescendingReverseThenByThenByDescendingUnion

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>,通常是从以下方法之一返回的:OrderByOrderByDescendingThenByThenByDescending。此方法提供了按序列中每个项的附加键值按升序对序列进行排序的功能。

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>,通常是从以下方法之一返回的:OrderByOrderByDescendingThenByThenByDescending。此方法提供了按序列中每个项的附加键值按降序对序列进行排序的功能。

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

单例方法

单例方法强制立即具体化至少一部分序列。这些方法包括:AggregateAllAnyAverageContainsCountElementAtElementAtOrDefaultFirstFirstOrDefaultLastLastOrDefaultLongCountMaxMinSequenceEqualSingleSingleOrDefaultSumToArrayToDictionaryToListToLookup

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日 - 添加了本系列第四篇文章的链接
© . All rights reserved.