关于枚举/列表的评估[重复]
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
是什么或代表什么的任何信息。事实上,这就是重点——你把这些细节抽象掉了。这些方法中有一些优化,但它们并没有做任何聪明的事情。最后,在您的示例中,List
和 IEnumerable
之间的区别并不是根本性的 - 这是 myEnumerable.Skip(1)
有副作用(因为 myEnumerable
本身有副作用)而 myList.Skip(1)
没有吨。但两者都做完全相同的事情 - 逐项评估可枚举。除了GetEnumerator
之外没有其他方法可以枚举,而IEnumerator
只有Current
和MoveNext
(对我们来说很重要)。
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 我对此进行了编辑以添加一个 hackyIEnumerable
来满足您的需求。请仅将其用于学习目的:-)
非常感谢,这有助于更好地理解它。无论如何,我要将我的问题标记为重复,因为我发现了这个:***.com/questions/12427097/…以上是关于关于枚举/列表的评估[重复]的主要内容,如果未能解决你的问题,请参考以下文章