Linq只保存操作
延迟执行
由Linq方法返回的IEnumerable接口,是一个藏起来的类型。
没有源码没法访问这个类型。这个类型里面记录了你的操作,但不会执行。
通过这个类型继续调用Linq方法,会把接下来的操作接在后面。
这些操作直到使用的时候,才一并执行。
操作包括foreach,和一些不返回Linq的方法,例如Max,All,Count,ToArray。
例如:
int[] p = { 1, 2, 3, 4, 5 };
var e = from a in p select a;
foreach (var item in e)
{
Console.WriteLine(item);
}
int[] p = { 1, 2, 3, 4, 5 };
var e = from a in p select a;
for (int i = 0; i < p.Length; i++)
{
p[i] = 0;
}
foreach (var item in e)
{
Console.WriteLine(item);
}
多次执行
基于上述原理,如果你多次执行Linq,那么foreach会执行多次。
class Foo : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator()
{
Console.WriteLine("你执行了一次查询");
for (int i = 0; i < 20; i++)
{
yield return i;
}
Console.WriteLine("查询结束");
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Foo f = new Foo();
var t = f.Select(s => s * s);
foreach (var item in t)
{
Console.WriteLine(item);
}
Console.WriteLine(t.Count());
Console.WriteLine(t.Max());//运行这段代码会出现三次"你执行了一次查询"。
所以如果Linq只使用一次,那么直接使用Linq。这样不会创建容器。
数组等容器创建起来都是占空间的。而Linq只占一个迭代器的空间。
而如果多次使用一个查询,那就应该把查询结果存起来。
Foo f = new Foo();
var t = f.Select(s => s * s).ToArray();
foreach (var item in t)
{
Console.WriteLine(item);
}
Console.WriteLine(t.Count());
Console.WriteLine(t.Max());//运行这段代码只会出现一次"你执行了一次查询"。
不要在Linq中间转容器
如果你保存的遍历是一个Linq,而这个Linq的操作中间有转容器操作,
那问题更大了。他会执行全程,而不是从转容器到最后的Linq之间。
var t = f.Select(s => s * s).ToArray().Reverse();
不仅如此,他还会每次执行都创建一个容器,消耗大量内存。
而且还要往容器里添加内容,最后转为对容器调用Linq,消耗大量时间。
不要用for循环操作Linq方法
在for循环中执行Linq方法将是灾难性的。
Foo f = new Foo();
var t = f.Select(s => s * s) ;
for (int i = 0; i < t.Count(); i++)
{
Console.WriteLine(t.ElementAt(i));
}
不要使用Skip和Take在for循环中截取元素
Foo f = new Foo();
var t = f.Select(s => s).ToArray();
for (int i = 0; i < t.Length / 3; i++)
{
var b = t.Skip(i * 3).Take(3);
foreach (var item in b)
{
Console.WriteLine(item);
}
Console.WriteLine();
}
Skip也要从头开始访问元素,而不是直接跳过。
所以应该改为数组或其他可以截取的容器进行访问。
你可以自定义Linq扩展方法
public static class Extend
{
public static IEnumerable<T> Even<T>(this IEnumerable<T> enumber)
{
bool b = false;
foreach (var item in enumber)
{
if (b)
yield return item;
b ^= true;
}
}
}
返回值是IEnumerable的方法也可以使用yield语法。
编译器会自动帮你合成出一个记录操作的IEnumerable,而不当场执行。
Linq只会从头开始遍历
由于迭代器是只会从头开始遍历的。所以Linq也只能从头开始遍历。
Linq内的Reverse方法也要在遍历全部元素后才能反转。
更不必说排序这种必须完全遍历才能执行的方法。
但是,如果一个方法只需要从头开始找到需要的值,那么不需要遍历完全程。
例如All,Any,FirstOrDefault,ElementAtOrDefault,Skip,Take方法。
他们找到自己感兴趣的值就会走。
而基于末尾和倒数的方法不行。例如LastOrDefault,TakeLast。
被过滤的元素不会执行后续操作
执行了筛选或截取操作,如Where,Skip,Take后不满足条件的元素不会再执行后续内容。
因此如果有这些操作,能往前摆的就往前摆。
有多个Where,SkipWhere,TakeWhere时,应该同时考虑能过滤的元素比例,和执行时间。
例如方法A可以过滤90%的元素,需要花费4的时间。
方法B可以过滤50%的元素,需要花费1的时间。
先A后B需要:4*100%+10%*1=410%
先B后A需要:1*100%+50%*4=300%
计算方法是:A发起比较,目标B
A的过滤比例*B的执行时间比较B的过滤比例*A的执行时间
90%*1比较50%*4
90%对比200%
B能节省更多时间,所以把B放在前面
选用合适的Linq方法
有多个条件的拆分,可以尝试使用分组
Foo f = new Foo();
var t = f.Where(s => s % 2 == 0).ToArray();
var t2 = f.Where(s => s % 3 == 0).ToArray();
var t3 = f.Where(s => s % 6 == 0).ToArray();
如果后续只需要对元素进行查询,可以使用ToLookup
var look = f.ToLookup(s => (s % 2 == 0, s % 3 == 0));
Console.WriteLine(look[(true, false)].Max());
否则,使用分组,并映射成容器。
var arr = f.GroupBy(s => (s % 2 == 0, s % 3 == 0)). ToDictionary(s=>s.Key,s=>s.ToArray());
Console.WriteLine(arr[(false,true)].Length);
使用联表找对应元素,而不要多次使用first查找元素
string[] arr1 = null;
string[] arr2 = null;
foreach (var item in arr1)
{
if (arr2.FirstOrDefault(s => s == item) is { } temp)
{
//操作找出来的数据
}
}
string[] arr1 = null;
string[] arr2 = null;
var t = arr1.Join(arr2, a => a, b => b, (a, b) => (a, b));
foreach (var item in t)
{
//操作item
}