关于枚举/列表的评估[重复]

Posted

技术标签:

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

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

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

我做了一些测试:

可枚举。 https://www.tutorialspoint.com/tpcg.php?p=bs75zCKL 名单:https://www.tutorialspoint.com/tpcg.php?p=PpyY2iif SingleEvaluationEnum:https://www.tutorialspoint.com/tpcg.php?p=209Ciiy7

从 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) 枚举器不起作用的原因。

谢谢!

【问题讨论】:

【参考方案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 来满足您的需求。请仅将其用于学习目的:-) 非常感谢,这有助于更好地理解它。无论如何,我要将我的问题标记为重复,因为我发现了这个:***.com/questions/12427097/…

以上是关于关于枚举/列表的评估[重复]的主要内容,如果未能解决你的问题,请参考以下文章

关于model.train()的困惑[重复]

根据关于通缉的字典的不完整信息在字典列表中查找字典 [重复]

关于字符串重复调用转换的优化方法

关于flutter列表的性能优化,你必须要了解的

我想要一些关于如何根据元素频率对列表进行排序的帮助[重复]

关于Java里的TreeSet判断重复元素。