【问题标题】:Regarding evaluation of Enumerable/List [duplicate]关于枚举/列表的评估[重复]
【发布时间】:2020-03-08 03:26:53
【问题描述】:

我一直在玩 Lists 和 Enumerables,我想我了解了基础知识:

  • 可枚举:每次使用元素时都会对其进行评估。
  • 列表:元素根据定义进行评估,不会在任何时候重新评估。

我做了一些测试:

从 Enumerable 示例开始:

var myList = new List<int>() { 1, 2, 3, 4, 5, 6 };
var myEnumerable = myList.Where(p =>
    {
        Console.Write($"{p} ");
        return p > 2;
    }
);

Console.WriteLine("");
Console.WriteLine("Starting");
myEnumerable.First();
Console.WriteLine("");
myEnumerable.Skip(1).First();

输出是:

Starting
1 2 3 
1 2 3 4 

如果我们在.Where(...) 之后添加.ToList(),则输出为:

1 2 3 4 5 6 
Starting

我也能够在这门课上两全其美:

class SingleEvaluationEnum<T>
{
    private IEnumerable<T> Enumerable;

    public SingleEvaluationEnum(IEnumerable<T> enumerable)
        => Enumerable = enumerable;

    public IEnumerable<T> Get()
    {
        if (!(Enumerable is List<T>))
            Enumerable = Enumerable.ToList().AsEnumerable();

        return Enumerable;
    }
}

可以看到输出是:

Starting
1 2 3 4 5 6 

这样,评估将推迟到第一次消费,并且不会在下一次消费中重新评估。但是整个列表都会被评估。

 

我的问题是:有没有办法得到这个输出?

Starting
1 2 3
4

换句话说:我希望myEnumerable.First() 只评估必要的元素,而不是更多。我希望myEnumerable.Skip(1).First() 重用已评估的元素。

编辑:澄清:我希望对 Enumerable 的任何“查询”都适用于列表中的所有元素。这就是 (AFAIK) 枚举器不起作用的原因。

谢谢!

【问题讨论】:

    标签: c# .net list lazy-evaluation enumerable


    【解决方案1】:

    LINQ 从根本上说是一种处理集合的函数式方法。假设之一是评估函数没有副作用。您在函数中调用 Console.Write 违反了该假设。

    没有魔法,只有函数。 IEnumerable 只有一种方法 - GetEnumerator。这就是 LINQ 所需的一切,而这正是 LINQ 真正所做的一切。例如,Where 的简单实现如下所示:

    public static IEnumerable<T> Where<T>(this IEnumerable<T> @this, Func<T, bool> filter)
    {
      foreach (var item in @this)
      {
        if (filter(item)) yield return item;
      }
    }
    

    Skip 可能如下所示:

    public static IEnumerable<T> Skip<T>(this IEnumerable<T> @this, int skip)
    {
      foreach (var item in @this)
      {
        if (skip-- > 0) continue;
    
        yield return item;
      }
    }
    

    仅此而已。它没有关于IEnumerable 是什么或代表什么的任何信息。事实上,这就是重点——你把这些细节抽象掉了。这些方法中有一些优化,但它们并没有做任何聪明的事情。最后,在您的示例中,ListIEnumerable 之间的区别并不是根本性的 - 这是 myEnumerable.Skip(1) 有副作用(因为 myEnumerable 本身有副作用)而 myList.Skip(1) 没有吨。但两者都做完全相同的事情 - 逐项评估可枚举。除了GetEnumerator 之外没有其他方法可以枚举,而IEnumerator 只有CurrentMoveNext(对我们来说很重要)。

    LINQ 是不可变的。这就是它如此有用的原因之一。这使您可以完全按照您正在做的事情 - 查询相同的可枚举两次但得到完全相同的结果。但你对此并不满意。你希望事情是可变的。好吧,没有什么能阻止你制作自己的辅助函数。毕竟,LINQ 只是一堆函数 - 你可以自己制作。

    这样一个简单的扩展可能是一个可记忆的枚举。环绕源枚举,在内部创建一个列表,当你迭代源枚举时,继续向列表中添加项目。下次调用 GetEnumerator 时,开始迭代您的内部列表。当您到达终点时,继续使用原始方法 - 遍历源可枚举并继续添加到列表中。

    这将允许您完全使用 LINQ,只需将 Memoize() 插入到您的 LINQ 查询中您希望避免多次迭代源的位置。在您的示例中,这将类似于:

    myEnumerable = myEnumerable.Memoize();
    
    Console.WriteLine("");
    Console.WriteLine("Starting");
    myEnumerable.First();
    Console.WriteLine("");
    myEnumerable.Skip(1).First();
    

    myEnumerable.First() 的第一次调用将遍历myList 中的前三项,而第二次仅对第四项有效。

    【讨论】:

    • 非常感谢,记忆是我一直在寻找的概念。通过它搜索,我找到了this other question,所以我将我的标记为重复。
    【解决方案2】:

    基本上听起来您正在寻找一个Enumerator,您可以通过在IEnumerable 上调用GetEnumerator 来获得它。 Enumerator 跟踪它的位置。

    var myList = new List<int>() { 1, 2, 3, 4, 5, 6 };
    var myEnumerator = myList.Where(p =>
        {
            Console.Write($"{p} ");
            return p > 2;
        }
    ).GetEnumerator();
    
    Console.WriteLine("Starting");
    myEnumerator.MoveNext();
    Console.WriteLine("");
    myEnumerator.MoveNext();
    

    这将为您提供输出:

    Starting
    1 2 3
    4
    

    编辑以回复您的评论: 首先,这听起来是一个非常糟糕的主意。枚举器代表可以枚举的东西。这就是为什么您可以在其之上通过管道传输所有那些花哨的 LINQ 查询。然而,所有对First 的调用“可视化”这个枚举(这导致调用GetEnumerator 以获得Enumerator 并遍历它,直到我们完成然后处理它)。但是,您要求每个可视化都更改它正在可视化的IEnumerable(这不是好的做法)。

    但是,既然您说这是为了学习,我将为您提供以IEnumerable 结尾的代码,这将为您提供所需的输出。我不建议您在实际代码中使用它,这不是一种好的和可靠的做事方式。

    首先我们创建一个自定义的Enumerator,它不会释放,只是不断枚举一些内部枚举器:

    public class CustomEnumerator<T> : IEnumerator<T>
    {
        private readonly IEnumerator<T> _source;
    
        public CustomEnumerator(IEnumerator<T> source)
        {
            _source = source;
        }
    
        public T Current => _source.Current;
    
        object IEnumerator.Current => _source.Current;
    
        public void Dispose()
        {
    
        }
    
        public bool MoveNext()
        {
            return _source.MoveNext();
        }
    
        public void Reset()
        {
            throw new NotImplementedException();
        }
    }
    

    然后我们创建一个自定义的IEnumerable 类,而不是每次调用GetEnumerator() 时都创建一个新的Enumerator,而是秘密地继续使用相同的枚举器:

    public class CustomEnumerable<T> : IEnumerable<T>
    {
        public CustomEnumerable(IEnumerable<T> source)
        {
            _internalEnumerator = new CustomEnumerator<T>(source.GetEnumerator());
        }
    
        private IEnumerator<T> _internalEnumerator;
        public IEnumerator<T> GetEnumerator()
        {
            return _internalEnumerator;
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return _internalEnumerator;
        }
    }
    

    最后我们创建一个IEnumerable 扩展方法来将IEnumerable 转换为我们的CustomEnumerable

    public static class IEnumerableExtensions
    {
        public static IEnumerable<T> ToTrackingEnumerable<T>(this IEnumerable<T> source) => new CustomEnumerable<T>(source);
    }
    

    我们现在终于可以这样做了:

    var myList = new List<int>() { 1, 2, 3, 4, 5, 6 };
    
    var myEnumerable = myList.Where(p =>
    {
        Console.Write($"{p} ");
        return p > 2;
    }).ToTrackingEnumerable();
    
    Console.WriteLine("Starting");
    var first = myEnumerable.First();
    Console.WriteLine("");
    var second = myEnumerable.Where(p => p % 2 == 1).First();
    Console.WriteLine("");
    

    我更改了最后一部分,以表明我们仍然可以在其上使用 LINQ。现在的输出是:

    Starting
    1 2 3
    4 5
    

    【讨论】:

    • 我认为这不一样。也许我没有正确解释它;这个想法是所有“查询”都适用于整个列表。 AFAIK,使用枚举器我不能使用 Skip() 和其他 LINQ 方法,对吧?
    • @raul.vila 我对此进行了编辑以添加一个 hacky IEnumerable 来满足您的需求。请仅将其用于学习目的:-)
    • 非常感谢,这有助于更好地理解它。无论如何,我要将我的问题标记为重复,因为我发现了这个:stackoverflow.com/questions/12427097/…
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-20
    • 1970-01-01
    • 1970-01-01
    • 2016-09-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多