所以我有一个相当标准的LINQ-to-Object设置。
var query = expensiveSrc.Where(x=> x.HasFoo)
.OrderBy(y => y.Bar.Count())
.Select(z => z.FrobberName);
// ...
if (!condition && !query.Any())
return; // seems to enumerate and sort entire enumerable
// ...
foreach (var item in query)
// ...
这列举了两次。这很糟糕。
var queryFiltered = expensiveSrc.Where(x=> x.HasFoo);
var query = queryFiltered.OrderBy(y => y.Bar.Count())
.Select(z => z.FrobberName);
if (!condition && !queryFiltered.Any())
return;
// ...
foreach (var item in query)
// ...
有效,但还有更好的方法吗?
是否有任何非疯狂的方式来“启发”Any()绕过非必需的操作?我想我记得这种优化进入了EduLinq。
答案 0 :(得分:9)
为什么不摆脱冗余:
if (!query.Any())
return;
它似乎没有任何目的 - 即使没有它,如果查询没有产生任何结果,foreach
的主体也不会执行。因此,通过Any()
签入,您可以在快速路径中保存任何内容,并在慢速路径中枚举两次。
另一方面,如果你必须知道在循环结束后是否找到了的结果,你也可以只使用一个标志:
bool itemFound = false;
foreach (var item in query)
{
itemFound = true;
... // Rest of the loop body goes here.
}
if(itemFound)
{
// ...
}
如果你真的担心循环体中的冗余标志设置,你可以直接使用枚举器:
using(var erator = query.GetEnumerator())
{
bool itemFound = erator.MoveNext();
if(itemFound)
{
do
{
// Do something with erator.Current;
} while(erator.MoveNext())
}
// Do something with itemFound
}
答案 1 :(得分:2)
编辑(修订):此答案回顾了执行两次查询的问题,我认为这是关键问题。见下文原因:
让Any()
更聪明是只有Linq实施者可以做的事情,IMO ...或者使用反射会有一些肮脏的冒险。
使用如下所示的类,您可以缓存原始可枚举的输出,并将其枚举两次:
public class CachedEnumerable<T>
{
public CachedEnumerable(IEnumerable<T> enumerable)
{
_source = enumerable.GetEnumerator();
}
public IEnumerable<T> Enumerate()
{
int itemIndex = 0;
while (true)
{
if (itemIndex < _cache.Count)
{
yield return _cache[itemIndex];
itemIndex++;
continue;
}
if (!_source.MoveNext())
yield break;
var current = _source.Current;
_cache.Add(current);
yield return current;
itemIndex++;
}
}
private List<T> _cache = new List<T>();
private IEnumerator<T> _source;
}
这样你就可以保持LINQ的惰性,保持代码的可读性和通用性。直接使用IEnumerator<>
会慢一些。有很多机会可以扩展和优化这个类,例如丢弃旧项目的政策,摆脱协程等。但是我认为这超出了这个问题的范围。
哦,这个类现在不是线程安全的。这并没有被问到,但我可以想象有人在尝试。如果源枚举没有线程关联,我认为可以很容易地添加它。
为什么这会是最佳的?
让我们考虑两个可能性:枚举可能包含元素,也可能不包含元素。
Where()
子句之后有零个项目,则排序零项,这将花费零时间(好吧,差不多)。 Select()
子句也是如此。 如果这还不够快怎么办?在这种情况下,我的策略是绕过Linq。现在,我真的很喜欢linq,但它的优雅是有代价的。因此,对于每100次使用Linq,通常会有一两个计算对于执行非常快速非常重要,我使用旧的for循环和列表编写。掌握技术的一部分是认识到不合适的地方。 Linq也不例外。
答案 2 :(得分:2)
可以从可枚举中提取的信息不多,所以将查询转换为IQueryable可能会更好吗?这个Any
扩展方法向下遍历其表达式树,跳过所有不相关的操作,然后将重要分支转换为可以调用以获得优化IQueryable的委托。标准Any
方法明确应用于它以避免递归。不确定极端情况,也许缓存已编译的查询是有意义的,但是像你这样的简单查询似乎有效。
static class QueryableHelper {
public static bool Any<T>(this IQueryable<T> source) {
var e = source.Expression;
while (e is MethodCallExpression) {
var mce = e as MethodCallExpression;
switch (mce.Method.Name) {
case "Select":
case "OrderBy":
case "ThenBy": break;
default: goto dun;
}
e = mce.Arguments.First();
}
dun:
var d = Expression.Lambda<Func<IQueryable<T>>>(e).Compile();
return Queryable.Any(d());
}
}
查询本身必须像这样修改:
var query = expensiveSrc.AsQueryable()
.Where(x=> x.HasFoo)
.OrderBy(y => y.Bar.Count())
.Select(z => z.FrobberName);
答案 3 :(得分:2)
是否有任何非疯狂的方式来“启发”Any()绕过非必需的操作?我想我记得这种优化进入了EduLinq。
好吧,我不会忽视提到Edulinq的任何问题:)
在这种情况下,Edulinq可能比LINQ to Objects更快,因为它的OrderBy
实现尽可能地懒惰 - 它只需要排序它需要的数量,以便检索它返回的元素
然而,从根本上说,仍然必须在返回任何内容之前读取整个序列。毕竟,序列中的最后一个元素可能是第一个必须返回的元素。
如果您控制整个堆栈,可以让Any()
检测到它已在您的“已知”IOrderedEnumerable
实施中被调用,并直接转到原始来源。请注意,此确实会在观察到的行为中产生更改 - 如果迭代整个序列会引发异常(或者有任何其他副作用),则优化会丢失该副作用。当然,你可以说这没关系 - 在LINQ中算作“有效”的优化是一个非常棘手的领域。
另一种非常可怕但可以解决这一特定问题的可能性是使IOrderedEnumerable
返回的迭代器只从源中获取MoveNext()
的第一个值。这对于Any
的正常实现来说已经足够了,在那时我们不需要知道第一个元素是什么。我们可以推迟实际排序,直到第一次使用Current
属性。
这是一个非常特殊的案例优化 - 而且我需要谨慎实施。我认为Ani的方法更好 - 只是使用query
使用foreach
进行迭代,如果查询结果为空,则永远不会进入循环体。
答案 4 :(得分:1)
试试这个:
var items = expensiveSrc.Where(x=> x.HasFoo)
.OrderBy(y => y.Bar.Count())
.Select(z => z.FrobberName).ToList();
// ...
if (!condition && items.Count == 0)
return; // Just check the count
// ...
foreach (var item in items)
// ...
查询只执行一次。
答案 5 :(得分:1)
但是我丢失了流量/延迟加载,这是linq的一半
延迟加载(延迟执行)和2个具有不同结果的LINQ查询无法优化(减少)为1次查询执行。
答案 6 :(得分:0)
为什么不使用.ToArray()
var query = expensiveSrc.Where(x=> x.HasFoo)
.OrderBy(y => y.Bar.Count())
.Select(z => z.FrobberName).ToArray();
如果没有元素,排序和选择不应给予太多开销。如果你正在排序,那么无论如何你都需要一个存储数据的缓存,所以开销.ToArray产生的开销不应该那么多。 如果你反编译OrderedEnumerable类,你会发现有一个包含引用的int []数组,所以你只需要使用.ToArray(或.ToList)创建一个新的引用数组。
BUT 如果expensiveSrc来自数据库,其他策略可能会更好。如果可以在数据库中完成排序,这会给你带来很大的开销,因为数据存储了两次。