关于Enumerable/List的评价

Regarding evaluation of Enumerable/List

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

我做了一些测试:

从可枚举示例开始:

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:

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 上的任何 "query" 都适用于列表中的所有元素。这就是(据我所知)枚举器不起作用的原因。

谢谢!

基本上听起来您正在寻找一个 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 "visualize" 这个枚举的调用(这导致 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 class,而不是每次调用 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

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;
  }
}

A 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 中的前三项,第二次调用仅与第四项一起工作。