使用LINQ的where(lamdaexp).First和First(lambdaexp)

4
下面这两种方法有什么区别吗?我得到的输出结果是一样的,但我想知道哪种方法是正确且有效的。
方法1:
Product product12 = products.Where(p => p.ProductID == 12).First();

方案2:

Product prod12 = products.First(p => p.ProductID == 12);

可能是重复问题:https://dev59.com/KWoy5IYBdhLWcg3wQ78F?rq=1 - Eris
4个回答

8

(我假设你正在使用Linq to .Net)
首先让我们看一下它们的源代码:

这里是Where()

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    if (source == null) 
        throw Error.ArgumentNull("source");
    if (predicate == null) 
        throw Error.ArgumentNull("predicate");
    if (source is Iterator<TSource>) 
        return ((Iterator<TSource>)source).Where(predicate);
    if (source is TSource[]) 
        return new WhereArrayIterator<TSource>((TSource[])source, predicate);
    if (source is List<TSource>) 
        return new WhereListIterator<TSource>((List<TSource>)source, predicate);
    return new WhereEnumerableIterator<TSource>(source, predicate);
}

这里是First()

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

让我们找出每个代码的作用:

  1. products.First(p => p.ProductID == 12);

根据 First() 的源代码,我们可以说,First() 会遍历集合,并在找到满足条件的第一项时停止遍历。

  1. products.Where(p => p.ProductID == 12).First();

首先它将在 Where 方法中创建迭代器,其中元素符合条件。然后,它将再次获取该迭代器的第一个元素。并且,只要找到第一个元素就返回它

作为附加说明,LINQ 在某些方法中使用延迟执行。它与你的问题结果有一定关系。

延迟执行是一种执行模式,CLR通过该模式确保仅在需要时从基于IEnumerable的信息源中提取值。当任何Linq运算符使用延迟执行时,CLR会将相关信息(例如原始序列、谓词或选择器(如果有))封装到一个迭代器中,在使用ToList方法或ForEach方法或手动使用C#中的底层GetEnumerator和MoveNext方法从原始序列中提取信息时使用该迭代器。

问题是:哪一个更快?


主要观点是,在处理列表和数组时,Where().FirstFirst() 更快。否则,First() 将更快。请参阅@Akash Kava的答案中的详细解释
让我们关注一下Where()的实现。如果你的集合是List,它将返回WhereListIterator(),但First()将直接迭代源代码。在我的看法中,他们在WhereListIterator实现中进行了一些加速。之后,我们调用First()方法,该方法不需要谓词作为输入,只会对过滤后的集合进行迭代。
此外,据我了解,Where迭代器避免了间接的虚表调用,而是直接调用迭代器方法。 这就是加速的原因。

Where+First不会遍历整个集合。 - MarcinJuraszek
@AkashKava 另外,如果你将它更改为 x.Size > 20,那么 Where().First() 将会非常快。 这是 fiddle:https://dotnetfiddle.net/qSLSrI - Farhad Jabiyev
@AkashKava 我在某些方面同意你的看法。但是,说实话,“Where().First()”通常比“First()”执行得更快。我以前进行过其他测试,并且记得在将近90%的测试中,“Where().First()”更快。但是,我无法解释为什么当筛选器为“<”时,“First()”执行得更快。我想找出原因。 - Farhad Jabiyev
@AkashKava 从我的角度来看,我已经找到了原因。<情况是特殊的,因为结果是集合中的第一个元素。尝试在查询之前添加:items.Reverse(); - Farhad Jabiyev
不,如果你看到我调用了items.OrderBy(x => new Guid()),这会随机排列集合。请查看我的新fiddle,在所有情况下,First都比Where First更快。 - Akash Kava
显示剩余4条评论

1
我刚刚进行了一次测试,你可以在.Net fiddle上查看结果,https://dotnetfiddle.net/3lCqsR 真正的答案是,哪个更快取决于应用的筛选器类型和迭代的集合类型。
    Func<Item,bool> filter = x => x.Size == 200;

    Run("First", () => items.First( filter ));
    Run("Where First", () => items.Where( filter ).First());

88 milliseconds for First
69 milliseconds for Where First


    filter = x => x.Size < 200;

    Run("First", () => items.First( filter ));
    Run("Where First", () => items.Where( filter ).First());

2 milliseconds for First
4 milliseconds for Where First


    filter = x => x.Size > 200;

    Run("First", () => items.First( filter ));
    Run("Where First", () => items.Where( filter ).First());

88 milliseconds for First
71 milliseconds for Where First

所以没有明确的答案。

另一个测试,https://dotnetfiddle.net/k11nX6,一直以来,First比Where First更快。

更新

在分析List类之后,发现Where().First()只比First()更快,在List和Array的情况下才适用,不适用于IQueryable或任何其他形式的Enumerable。原因是,在List中使用的Enumerator未缓存每个线程。List总是创建新的Enumerator,因此First()使用List的Enumerator(迭代器)。

Where()中使用的WhereListIterator用于List,不会创建新的Enumerator,而是将当前Iterator缓存在线程中。这使得Where().First()运行更快。

然而,缓存WhereListIterator的操作具有一定的成本,因此对于较小的集合和产生较小结果的某些条件,Where().First()会更慢。

如果您查看此示例,则First每次都会击败Where First。

https://dotnetfiddle.net/OrUUSG


说实话,我已经重新运行了它。当筛选器为 filter = x => x.Size < n; 时,First 只会更快。尝试添加 items.Reverse() - Farhad Jabiyev
你试过 https://dotnetfiddle.net/k11nX6 吗?还要注意一下有一个随机排序,所以项目没有被排序。 - Akash Kava
是的,因此我们可以说,一般来说 Where().First() 会更快。这是因为 Where 迭代器避免了间接的虚表调用,而是直接调用迭代器方法。 当然,我们也可以找到 First() 更快的情况。但是,如果有人有时间并且能够搜索并告诉我们背后具体的原因,我会非常高兴。 - Farhad Jabiyev
1
@FarhadJabiyev,新的更新,Where().First() 只对 List 和 Array 更快,对于其他可枚举对象来说并不一定更快。请查看我的更新。 - Akash Kava
非常感谢您提供的宝贵信息。我会在我的回答中添加您的链接。 - Farhad Jabiyev
@Akash kava,感谢您的精彩分析。我将尝试在fiddle中使用更多的组合。 - hanuma

0

它们在功能上是等效的,同样有效。LINQ构建一个查询,直到结果被枚举(例如foreach循环或在这种情况下的First()操作)才进行评估。

因此,两者都将按原始顺序评估项目,直到找到第一个ProductID == 12的实例,并返回该实例(如果找到则抛出异常)。

话虽如此,后一种方法是我更喜欢的方法,因为它更简洁,通常更易于阅读。


0

我认为你的问题更像是:

  1. Where 在到达 First 之前会遍历整个集合吗?
  2. First 也会这样做吗?

我编写了自己的 WhereFirst 实现。
这个例子不是精确的 .net 实现,但它在相同的概念下工作。

public static class Extensions
{
    public static IEnumerable<T> MyWhere<T>(this IEnumerable<T> collection, Func<T,bool> condition)
    {
        foreach(var item in collection)
        {
            if(condition(item))
            {
                yield return item;
            }
        }
    }

    public static T MyFirst<T>(this IEnumerable<T> collection, Func<T,bool> condition)
    {
        foreach (var item in collection)
        {
            if (condition(item))
            {
                return item;
            }
        }

        throw new InvalidOperationException("No element found");
    }
}

执行以下代码:
List<int> myList = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var five = myList.MyWhere(i => i < 6).MyFirst(i => i == 5);

MyWhereMyFirstforeach内部放置一个调试器断点,并开始理解使用Linq To Objects迭代器时发生的情况。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接