8

假设我有一些字符串:

string[] strings = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" };

有什么区别:

string startsWithO = strings.First(s => s[0] == 'o');

和:

string startsWithO = strings.Where(s => s[0] == 'o').First();

由于 Where() 被推迟,它不应该减慢执行速度,对吧?

4

3 回答 3

12

.Where(filter).First()使用而不是使用的性能损失.First(filter)通常非常小。

然而,它们并不相同——Where生成一个新的迭代器,它First可以简单地获取一个元素,而First(filter)可以通过只使用一个循环和filter匹配时直接返回来进行微优化。

因此,虽然这两种方法具有相同的语义并且都filter同样频繁地执行(仅在必要时执行),但使用Firstwithfilter参数不需要创建中间迭代器对象,并且可能也避免了对该迭代器的一些非常简单的方法调用。

换句话说,如果你正在执行这样的代码数百万次,你会看到轻微的性能差异——但没什么大不了的;我永远不会担心它。每当这种微小的性能差异实际上很重要时,您最好只编写等效的(非常简单的)foreach-with-if 语句并避免 LINQ 中固有的额外调用和对象分配 - 但请记住,这是一个微优化,您将很少需要。

编辑:基准演示效果:

这需要 0.78 秒:

for(int i=0;i<10*1000*1000;i++)
  Enumerable.Range(0,1000).First(n=> n > 2);
GC.Collect();

但这需要 1.41 秒:

for(int i=0;i<10*1000*1000;i++)
  Enumerable.Range(0,1000).Where(n=> n > 2).First();
GC.Collect();

而普通循环要快得多(0.13 秒):

long bla = 0;
for(int i=0;i<10*1000*1000;i++)
    for(int n=0;n<1000;n++)
        if(n > 2) { bla+=n; break; }
GC.Collect();
Console.WriteLine(bla);//avoid optimizer cheating.

请注意,此基准仅显示如此极端的差异,因为我有一个微不足道的过滤器和一个非常短的不匹配前缀。

基于一些快速的实验,差异似乎很大程度上归因于采用哪个代码路径的细节。因此,对于数组和List<>s,第一个变体实际上更快,可能.Where对那些没有的类型进行特殊处理First;对于自定义迭代器,第二个版本比预期的要快一点。

概括:

.Where(...).First()大致与 .First(...)- 不要费心选择一个或另一个作为优化一样快。一般来说 .First(...)速度会稍微快一些,但在某些常见情况下会更慢。如果你真的需要微优化,那么使用比任何一个都快的普通循环。

于 2013-01-10T16:09:39.360 回答
1

这里没有区别。

调用 Where first 返回一个迭代器,直到 First 开始循环它。

如果谓词不匹配任何元素,则会引发相同的异常 InvalidOperationException。

唯一的区别是代码的冗长,所以 .First 没有 .Where 应该是首选

于 2013-01-10T16:09:11.587 回答
1

在特定情况下,调用Firstand Whereon a string[],调用的方法是 theEnumerable.WhereEnumerable.First扩展方法。

Enumerable.Where做这个:

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) 
{
  // null checks omitted
  if (source is TSource[]) 
     return new WhereArrayIterator<TSource>((TSource[])source, predicate); 
  //the rest of the method will not execute
}

并且构造函数WhereArrayIterator只是:

public WhereArrayIterator(TSource[] source, Func<TSource, bool> predicate) {
  this.source = source; 
  this.predicate = predicate;
} 

所以这里实际上什么也没做,除了创建一个迭代器。

First一种方法,没有谓词这样做:

public static TSource First<TSource>(this IEnumerable<TSource> source) { 
  //null check
  IList<TSource> list = source as IList<TSource>;
  if (list != null) {
     //this branch is not taken as string[] does not implement IList<string>
     if (list.Count > 0) return list[0]; 
  }
  else { 
    //this is actually the WhereArrayIterator from before
    using (IEnumerator<TSource> e = source.GetEnumerator()) { 
      if (e.MoveNext()) 
        return e.Current;
    } 
  }
  throw Error.NoElements();
}

但是,第二个First这样做

public static TSource First<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
   //null checks
   foreach (TSource element in source) {
     if (predicate(element)) return element; 
   }
   throw Error.NoMatch();
}

在数组的情况下,它与直接线性访问一样快。
简而言之,这意味着调用First(predicate)数组会更快一些,速度不会很大,但仍然可以检测到。这可能不适用于列表,当然也不适用于IQueryable对象,这是一个完全不同的故事。

然而,这是最糟糕的微优化。除非这样做数百万次,否则不会节省太多秒。即使我现在知道这一点,我仍然会使用更清晰的内容来阅读和理解。

于 2013-01-10T16:49:44.340 回答