IEnumerable 方法管道
代码来自 https://ayende.com/blog/3082/管道和过滤器的可枚举方法
问题 已注册 3 个操作。 首先获取系统中的所有进程。 第二个过滤过程。 第三个写入进程名称。
但使用的是yield和GetEnumerator。
current = operation.Execute(current);
行被执行三次,第一个操作的输出列表是第二个操作的输入。而且它只是一个列表(操作)。
我不明白 private readonly List
???因为 foreach 有 3 个执行。执行 enumerator.MoveNext() 时执行的某种委托?好的,但是我们有 Foreach,它是执行的吗?在使用 GetEnumerator 之前三次。
所以列出“操作”应该是 ovverriden ...? :)
我想了解一种机制。在使用 enumerator.MoveNext() 执行之前如何存储此“委托”。以及它的用途是什么?
class Program
{
static void Main(string[] args)
{
var trivialProcess = new Pipeline<Process>();
trivialProcess.Register(new GetAllProcesses());
trivialProcess.Register(new LimitByWorkingSetSize());
trivialProcess.Register(new PrintProcessName());
trivialProcess.Execute();
}
}
interface IOperation<T>
{
IEnumerable<T> Execute(IEnumerable<T> input);
}
class GetAllProcesses : IOperation<Process>
{
public IEnumerable<Process> Execute(IEnumerable<Process> input)
{
Debug.WriteLine("GetAllProcesses Execute");
return Process.GetProcesses();
}
}
class LimitByWorkingSetSize : IOperation<Process>
{
public IEnumerable<Process> Execute(IEnumerable<Process> input)
{
int maxSizeBytes = 50 * 1024 * 1024;
Debug.WriteLine("LimitByWorkingSetSize Enter");
foreach (Process process in input)
{
Debug.WriteLine("LimitByWorkingSetSize foreach");
if (process.WorkingSet64 > maxSizeBytes)
{
Debug.WriteLine("LimitByWorkingSetSize yield");
yield return process;
}
}
}
}
class PrintProcessName : IOperation<Process>
{
public IEnumerable<Process> Execute(IEnumerable<Process> input)
{
Debug.WriteLine("PrintProcessName Enter");
foreach (Process process in input)
{
Debug.WriteLine("PrintProcessName print");
Console.WriteLine(process.ProcessName);
}
Debug.WriteLine("PrintProcessName break");
yield break;
}
}
class Pipeline<T>
{
private readonly List<IOperation<T>> operations = new List<IOperation<T>>();
public Pipeline<T> Register(IOperation<T> operation)
{
operations.Add(operation);
return this;
}
public void Execute()
{
IEnumerable<T> current = new List<T>();
foreach (IOperation<T> operation in operations)
{
current = operation.Execute(current);
}
IEnumerator<T> enumerator = current.GetEnumerator();
while (enumerator.MoveNext()) ;
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
它是一个
IOperation
列表,在本例中是GetAllProcesses
、LimitByWorkingSetSize
或PrintProcessName
类的实例。所有这些类都实现IOperation
接口,因此能够被视为IOperation
,并且允许存储在列表中。是的,但这只是设置了
LimitBy
和PrintProcesses
类的调用链,因为它们的Execute
是生成器(功能yield
) - 它们只有在被枚举时才会被执行。current = operation.Execute(current);
将立即执行GetAll
的Execute
,因为这不会yield< /code> 任何内容(但可以对其进行修改),但在 PrintProcess 中运行代码(依赖于 LimitBy),仅当
MoveNext()
发生时才会发生。PrintProcess
捕获控制流,并且在枚举时不会产生
任何内容。我怀疑这是在深入研究这种情况下的收益机制(但你在下面再次询问它,所以..)
给出的代码示例中有很多
foreach
。执行 PrintProcess 的 Execute 会运行一个 foreach,它枚举链中下一个方法(LimitBy
的Execute
)的输出 - 它本身也包含一个foreach
。如果您在调试器中单步执行,并在每个foreach
下的{
上设置断点,您将看到它在PrintProcess
和限制
;LimitBy
正在生成PrintProcess
正在枚举的某些循环传递的数据。PrintProcesses
不会为进一步的操作产生
任何东西,因此控制在这两个操作之间来回传递,直到LimitBy
没有任何东西可以提供,此时,PrintProcess
将不再枚举任何内容我怀疑这是一个重写错误。代码中没有任何内容会覆盖
operations
列表;它是被覆盖的当前
。它只是作为记住上次输出是什么的一种方式,因此它可以在每次循环时成为输入。严格来说,一开始就没有必要将其初始化为新的List
,因为该链中的第一个Execute
不会对其输入执行任何操作。代码>
我认为你本质上是在问
yield
是如何工作的;简而言之,编译器会为您编写一些类,这些类实现了一个枚举器,该枚举器根据您使用的围绕yield
关键字的逻辑生成值。长答案是相当可怕的,而不是你想自己写的东西,这就是为什么我们通常非常乐意让编译器生成它而不是在引擎盖下查看:为了我的钱,我可能会留下所有仅此而已:
这里有一些关于 yield 的内容:
当一个方法使用 yield 返回某些内容时,可以返回到该方法并从我们离开的地方继续执行离开。看看这个不产生结果的方法:
当我们调用这个方法时,我们只会得到 1。第一次返回是硬停止;第二次返回是硬停止。我们可以调用它一百万次,但只有第一次返回才会起作用。所有其他代码完全无法访问。
现在让我们改变它,使其产生:
现在它是一个可枚举的,所以现在我们将其称为将枚举它的东西的一部分:
编译器做了很多事情来将您的 foreach 转换为获取枚举器并调用 movenext 的 while一遍又一遍,但关键是,当您枚举此生成方法时,它
yield return 2
之后返回方法,return 3所以当我们让出时,就像它在该点的方法中放置了一个标记,返回到调用者,当我们返回到方法时(通过移动沿着 one 的迭代器),我们从我们停止的地方开始,而不是从方法的第一行开始
。您可以编写一个可以永远枚举的方法:
每次达到
yield
时,C# 都会返回到具有新值的枚举代码。因为这是在一个循环中,所以它会继续生成斐波那契数列,直到它爆炸(如果您检查了数学,或者您填满了内存,或者如果您只是打印,则加法会导致溢出)永远走下去)。为了能够停止枚举,您需要从方法返回而不产生屈服(超出方法的末尾),或者执行yield break
It's a List of
IOperation<T>
, in this case an instance of theGetAllProcesses
,LimitByWorkingSetSize
orPrintProcessName
classes. All these classes implement theIOperation<T>
interface and hence are capable of being treated as anIOperation<T>
, and are permitted to be stored in the list.Yes but that just sets up the chain of calls for the
LimitBy
andPrintProcesses
classes because theirExecute
s are generators (featureyield
) - they'll only be executed when they're enumerated. Thecurrent = operation.Execute(current);
will execute theExecute
ofGetAll
right away, because that doesn'tyield
anything (but it could be modified so it did) but running the code in PrintProcess, which leans on LimitBy, only happens when thatMoveNext()
occurs.PrintProcess
traps control flow and doesn'tyield
anything while it's enumerating.I suspect that's delving a bit deep into the mechanics of yield for this context (but you ask about it again below, so..)
There are lots of
foreach
es in the code sample given. Executing the PrintProcess's Execute runs a foreach, which enumerates the output of the next method down in the chain (LimitBy
'sExecute
) - which in itself also contains aforeach
. If you single step it in the debugger with a breakpoint on the{
under everyforeach
you'll see it jumping back and forth betweenPrintProcess
andLimitBy
;LimitBy
is generating data on some loop passes thatPrintProcess
is enumerating.PrintProcesses
doesn'tyield
anything to a further-up operation so control passes back and forth between these two untilLimitBy
has nothing left to give, at which point there will be no more things forPrintProcess
to enumerateI suspect that's a typo of overwritten. There isn't anything in the code that overwrites the
operations
list; it'scurrent
that's overwritten. It merely serves as a way of remembering what the output was last time so it can become an input this time on every pass of the loop. It wasn't strictly necessary to init it to a newList
at the outset because the firstExecute
in this chain doesn't do anything with itsinput
I think you're essentially asking how
yield
works; the short short version is that the compiler writes some classes for you that implement an enumerator that generates values based on the logic you used surrounding theyield
keyword. The long answer is fairly horrific and not something you'd want to write yourself, which is why we're usually more than happy to let the compiler generate it and not look under the hood:And for my money I'd probably leave all this alone and just do:
Here's a bit on
yield
:When a method returns something using
yield
, it is possible to get back into the method and carry on from where we left off. Look at this method that doesn't yield:When we call this, we'll only ever get 1. The first return is a hard stop; we could call it a million times and only the first return ever does anything. All the other code is totally unreachable.
Now let's change it so it yields:
It's a enumerable now, so now we call it as part of something that will enumerate over it:
There's a bunch of stuff the compiler does to transform your foreach into a while that gets an enumerator and calls movenext over and over but the crux of it is, when you enumerate over this yielding method it
yield return 2
, return 3So when we yield, it's like it places a marker in the method at that point, goes back to the caller, and when we come back into the method (by moving the iterator along one), we start at the marker from where we left off, rather than from the first line of the method
You don't have to hard code your yields; you can write a method that you can enumerate forever:
Every time it hits
yield
, C# goes back to the enumerating code with the new value. Because this is in a loop it'll just keep generating Fibonacci til it blows up (the addition results in an overflow if you have checked math on, or you fill up your memory.. or if you're just printing, it'll go forever). To be able to stop enumerating you either need to return from the method without yielding(fall out of the end of the method), or do ayield break