上一篇
下面我们为MyLinq添加第二个有用的方法,SelectMany,这个方法的作用是,选取序列中的每个元素,每个元素转换成一个序列,再把这些选取的序列连起来,得到一个整体的序列作为结果输出。
我们用SelectMany来遍历交错数组:
static void Main(string[] args)
{
int[][] data = new int[][]
{
new int[] { 1, 2, 3 },
new int[] { 4, 5 },
new int[] { },
new int[] { 6, 7, 8, 9 },
new int[] { 10 }
};
var result = data.SelectMany(x => x);
foreach (int item in result)
Console.WriteLine(item);
}
我们希望输出 1 2 3 4 5 6 7 8 9 10
这是代码框架,请完成:
public static IEnumerable<TResult> SelectMany<TInput, TResult>(this IEnumerable<TInput> data, Func<TInput, IEnumerable<TResult>> selector)
{
在此添加代码
}
完成的代码应该类似这样:
public static IEnumerable<TResult> SelectMany<TInput, TResult>(this IEnumerable<TInput> data, Func<TInput, IEnumerable<TResult>> selector)
{
foreach (TInput item in data)
{
foreach (TResult x in selector(item))
{
yield return x;
}
}
}
SelectMany可以用来获取两个集合的笛卡尔积,比如说,我们有这样两个数组:
string[] colors = { "红", "黄", "蓝", "绿" };
string[] goods = { "衣服", "帽子", "鞋子" };
我们用一种颜色搭配一种东西,一共有多少种组合方式?我们可以用SelectMany实现:
var result = colors.SelectMany(x => goods.Select(y => x + y));
foreach (string item in result)
{
Console.WriteLine(item);
}
程序输出如下:
红衣服
红帽子
红鞋子
黄衣服
黄帽子
黄鞋子
蓝衣服
蓝帽子
蓝鞋子
绿衣服
绿帽子
绿鞋子
很多时候,我们都会在SelectMany中包含一个Select操作,因为这种情况如此常用,我们再编写一个重载形式,将SelectMany和Select二合一。
public static IEnumerable<TResult> SelectMany<TInput, TCollection, TResult>(
this IEnumerable<TInput> data,
Func<TInput, IEnumerable<TCollection>> selector,
Func<TInput, TCollection, TResult> resultSelector
)
{
return data.SelectMany(x => selector(x).Select(y => resultSelector(x, y)));
}
那么我们可以将调用进一步简化成:
var result = colors.SelectMany(x => goods, (x, y) => x + y);
下面我们编写第三个方法,where,它的作用是,过滤输入的序列,去掉不符合条件的,将符合条件的元素组成新的序列输出。
public static IEnumerable<T> Where<T>(this IEnumerable<T> data, Func<T, bool> predicate)
{
foreach (T item in data)
if (predicate(item)) yield return item;
}
我们来调用下:
static void Main(string[] args)
{
var processNames = System.Diagnostics.Process.GetProcesses()
.Where(x => x.PeakPagedMemorySize64 > 40000000L)
.Select(x => x.ProcessName);
foreach (string processName in processNames)
{
Console.WriteLine(processName);
}
}
这段代码可以列出当前计算机上运行的进程中占用的峰值内存大于40MB(大约)的进程。
它在我的计算机上运行结果如下:
Microsoft.Alm.Shared.Remoting.RemoteContainer.dll
iexplore
svchost
MsMpEng
explorer
svchost
svchost
devenv
System
WINWORD
桌面、Word、VS开发环境“榜上有名”。你也可以用这段代码看看你的计算机上什么程序消耗了很多内存。
我们还是用之前的代码举例:
static void Main(string[] args)
{
string s = @"1,张三,52
2,李四,37
3,王五,47";
string[] data = s.Split(new string[] { "\r\n" }, StringSplitOptions.None);
var people = data.Select(x => new Person()
{
ID = int.Parse(x.Split(',')[0]),
Name = x.Split(',')[1],
Age = int.Parse(x.Split(',')[2])
});
foreach (Person p in people)
{
Console.WriteLine(p.Name);
}
}
这段代码不是将输入的字符串转化为Person对象了么,我们现在想知道的是,王五多少岁,怎么写?
var result = people.Where(x => x.Name == "王五");
foreach (Person p in people)
{
Console.WriteLine(p.Name);
}
注意,Where返回的是一个序列,哪怕只找到一个元素。所以不能这么写:
Person result = people.Where(x => x.Name == "王五");
Console.WriteLine(result.Name);
如果我们确定查找的元素只有一个(比如根据Name找人),或者找到很多匹配的,但是我们确实只需要一个结果就可以,这么写比较麻烦。我们封装一个First方法来简化上面的代码吧。
public static T First<T>(this IEnumerable<T> data)
{
var e = data.GetEnumerator();
e.MoveNext();
return e.Current;
}
还记得MoveNext和Current么?如果不记得了,可以复习下。在这里,我们只取第一个结果,就不用foreach了。
于是我们可以这样调用了:
Person result = people.Where(x => x.Name == "王五").First();
Console.WriteLine(result.Name);