【问题标题】:Where vs. foreach with if - why different results?where vs. foreach with if - 为什么不同的结果?
【发布时间】:2017-08-25 18:29:21
【问题描述】:

这段代码

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApplication
{
    internal class Program
    {
        public static void Main()
        {
            var values = new[] {1, 2, 3, 3, 2, 1, 4};
            var distinctValues = GetDistinctValuesUsingWhere(values);
            Console.WriteLine("GetDistinctValuesUsingWhere No1: " + string.Join(",", distinctValues));
            Console.WriteLine("GetDistinctValuesUsingWhere No2: " + string.Join(",", distinctValues));
            distinctValues = GetDistinctValuesUsingForEach(values);
            Console.WriteLine("GetDistinctValuesUsingForEach No1: " + string.Join(",", distinctValues));
            Console.WriteLine("GetDistinctValuesUsingForEach No2: " + string.Join(",", distinctValues));
            Console.ReadLine();
        }

        private static IEnumerable<T> GetDistinctValuesUsingWhere<T>(IEnumerable<T> items)
        {
            var set=new HashSet<T>();
            return items.Where(i=> set.Add(i));
        }

        private static IEnumerable<T> GetDistinctValuesUsingForEach<T>(IEnumerable<T> items)
        {
            var set=new HashSet<T>();
            foreach (var i in items)
            {
                if (set.Add(i))
                    yield return i;
            }
        }
    }
}

产生以下输出:

GetDistinctValuesUsingWhere No1: 1,2,3,4

GetDistinctValuesUsingWhere No2:

GetDistinctValuesUsingForEach No1: 1,2,3,4

GetDistinctValuesUsingForEach No2: 1,2,3,4

我不明白为什么在“GetDistinctValuesUsingWhere No2”行中没有得到任何值。

谁能给我解释一下?

更新在 Scott 回答后,我将示例更改为以下内容:

       private static IEnumerable<T> GetDistinctValuesUsingWhere2<T>(IEnumerable<T> items)
    {
        var set = new HashSet<T>();
        var capturedVariables = new CapturedVariables<T> {set = set};

        foreach (var i in items)
            if (capturedVariables.set.Add(i))
                yield return i;
        //return Where2(items, capturedVariables);
    }

    private static IEnumerable<T> Where2<T>(IEnumerable<T> source, CapturedVariables<T> variables)
    {
        foreach (var i in source)
            if (variables.set.Add(i))
                yield return i;
    }

    private class CapturedVariables<T>
    {
        public HashSet<T> set;
    }

这将导致输出 1、2、3、4 的两倍。

但是,如果我只是取消注释该行

return Where2(items, captureVariables);

并注释行

foreach(项目中的变量 i) if (capturedVariables.set.Add(i)) 收益回报 i;

在GetDistinctValuesUsingWhere2方法中,我只会得到一次输出1,2,3,4。这尽管删除的行和现在未注释的方法完全相同。

我还是不明白....

【问题讨论】:

  • 离题,但为什么不直接使用Distinct(或者对于具有属性的对象,MoreLINQ 的DistinctBy)而不是自己滚动?或者这仅仅是一个学习练习?
  • 尝试在 Where 末尾添加 .ToList(),您应该会得到正确的结果。问题是您正在返回表达式,因此每次请求对象时都会重新评估它。
  • 当然我会在现实生活中使用 Distinct。这只是我们最近偶然发现的一个更复杂问题的简化示例。
  • 重新编辑:您熟悉closures 的概念吗?

标签: c# .net closures


【解决方案1】:

GetDistinctValuesUsingWhere No2 没有返回任何结果的原因是因为变量捕获。

你的 where 方法更像这个函数

    private static IEnumerable<T> GetDistinctValuesUsingWhere<T>(IEnumerable<T> items)
    {
        var set=new HashSet<T>();
        var capturedVariables = new CapturedVariables {set = set}
        return Where(items, capturedVariables);
    }

    IEnumerable<T> Where(IEnumerable<T> source, CapturedVariables variables)
    {
        foreach (var i in items)
        {
            if (variables.set.Add(i))
                yield return i;
        }

    }

所以这两种方法在底层都是yield return,但是 GetDistinctValuesUsingWhere 会为每次调用重用哈希集,其中 GetDistinctValuesUsingForEach 每次枚举都会生成一个新的哈希集。

【讨论】:

  • 问题是,我不明白 CLR 为什么或在哪里决定它需要保持捕获的变量处于活动状态。如果您只是在“Where”方法中内联代码,则不会捕获变量,并且该示例在多次调用时按预期工作。 (见我更新的例子)。
  • @umei 带有yield return 语句的方法将“保持活动”至少直到枚举结束(整个方法将保持活动状态)。或者换句话说,可枚举表示整个方法(整个方法转换为状态机)。对于foreach 版本,lambda 表达式捕获对HashSet 的引用,并且可枚举仅表示该lambda 与Where 的应用,而不是HashSet 的实例化。您需要再次调用GetDistinctValuesUsingWhere 才能获得新的HashSet
【解决方案2】:

回答更新版本:

  • 对于包含foreachGetDistinctValuesUsingWhere2() 方法 循环,返回的IEnumerable 捕获了闭包中方法的全部内容,包括set 初始化语句。因此,每次您开始迭代可枚举时都会执行此语句,但不是在最初调用 GetDistinctValuesUsingWhere2() 期间。
  • 在另一个变体的情况下,您返回Where2()GetDistinctValuesUsingWhere2() 方法不需要捕获该方法的内容,因为您没有在其中定义迭代器或委托。相反,您将Where2() 作为IEnumerable 返回。 后一种方法仅捕获foreach 循环及其参数(已初始化),但不捕获set 初始化语句本身。因此,这一次,set 初始化语句只会在最初调用GetDistinctValuesUsingWhere2() 期间执行一次。

如有必要,请在代码中的不同点放置一些断点:这应该有助于您理解我在这里试图解释的内容。

【讨论】:

  • 我觉得这个答案更好地解释了这两种不同的方法。
猜你喜欢
  • 2022-09-28
  • 1970-01-01
  • 2019-05-20
  • 2018-04-25
  • 2020-12-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-01
相关资源
最近更新 更多