December 29
Yield与代码协同 (Coroutine)
C# 2.0 是在不修改 CLR 的前提下由编译器通过有限状态机来实现 iterator block 中 yield 关键字的。
从而实现将控制和行为分离。
实际上,这一机制的最终目的是提供一个代码协同(Coroutine)执行的支持机制。参见如下的代码,更能
说明问题:
using System.Collections.Generic;
public class Tokens : IEnumerable<string>
{
//iterator block,实现控制枚举的元素
public IEnumerator<string> GetEnumerator()
{
for(int i = 0; i<elements.Length; i++)
yield elements[i];
}
...
}
foreach (string item in new Tokens())
{
//实现终端上打印元素的行为
Console.WriteLine(item);
}
在这段代码执行过程中,foreach 的循环体和 GetEnumerator 函数体实际上是在同一个线程中交替执行的。这是一种介于线程和顺序执行之间的协同执行模式,之所以称之为协同(Coroutine),是因为同时执行的多个代码块之间的调度是由逻辑隐式协同完成的。
就协同执行而言,从功能上可以分为行为、控制两部分,控制又可进一步细分为控制逻辑和控制状态。行为对应着如何处理目标对象,如上述代码中:行为就是将目标对象打印到终端;控制则是如何遍历这个 elements 数组,可进一步细分为控制逻辑(顺序遍历)和控制状态(当前遍历到哪个元素)。
其中心思想就是将行为与控制分离,以此来降低代码的耦合度,增强通用性,提高代码的复用率。
Delphi中Yield的使用
让我们先看下面一段代码,producer过程(生产者)产生一些数值,而Consumer过程(消费者)则处理值:
procedure producer();
var
i: integer;
begin
for i := 0 to 100 do
if i mod 5 = 0 then consumer(i);
end;
procedure Consumer(const value: integer);
begin
writeln(value);
end;
代码似乎很完美,但是在生产者(producer)调用消费过程(consumer)这里出现了耦合,我们希望生产者过程(producer)能增强通用性,降低耦合度,能为不同的消费者服务。
要达到上述要求,必须要在一个地方维护控制的状态:记住枚举时候的状态。在消费者企图调用生产者产生的值时,生产者不得不在这个地方记住在调用之间的状态。
在Delphi中我们使用 YieldObj 作为维护控制的状态的地方。请看代码:
procedure Producer(YieldObj: TMeYieldObject);
var
i: integer;
begin
for i := 0 to 100 do
if i mod 5 = 0 then YieldObj.Yield(i);
end;
procedure Consumer();
function GetEnumerator: TYieldInteger;
begin
Result:= TYieldInteger.Create(Producer);
end;
begin
with GetEnumerator do
try
While MoveNext do
writeln(Current);
finally
Free;
end;
end;
执行Yield方法将退出Producer并返回"i"值作为Yield的参数,不同的是,当Yield返回时候,将会接着上次Yield退出的地方接着执行。
Yield 的作用就是保持当前状态(局部变量的值),然后返回行为逻辑控制,当下一次执行能接着状态执行。
消费者函数仅仅是通过MoveNext简单的调用生产者函数抓住每一个Yield的值。只要你想的话,你也可以对所有Producer产生的值进行求和:
procedure Consumer();
var
Sum: integer;
begin
sum := 0;
with TYieldInteger.Create(Producer) do
try
While MoveNext do
begin
Inc(Sum, Current);
end;
finally
Free;
end;
writeln(Sum);
end;
在Delphi2005(D9)以上版本上可以用For..In循环,这样来写(不过为了兼容性,还是使用上述的代码放心):
type
TMyContainer = class
public
function GetEnumerator: TYieldInteger;
end;
function TMyContainer.GetEnumerator: TYieldInteger;
begin
Result:= TYieldInteger.Create(Producer);
end;
procedure Consumer();
var
i: integer;
Sum: integer;
vMyContainer: TMyContainer;
begin
sum := 0;
vMyContainer := TMyContainer.Create;
try
for i in vMyContainer do Inc(Sum, i);
finally
vMyContainer.Free;
end;
writeln(Sum);
end;
在Delphi上实现 Yield
本实现是由俄罗斯的牛人 [http://santonov.blogspot.com/ Sergey Antonov] (or Антонов Сергей - aka. 0xffff) 具体实施的。
本人只是将其稍作修改并移植到了MeObject上。你可以在http://code.google.com/p/meaop/source 中的MeObjects/src/uMeYield.pas中找到。
不过如果你想使用delphi class类型,你需要打开MeSetting.inc中的 YieldClass_Supports 编译开关。
在MeObject上的使用
首先确保关闭MeSetting.inc中的 YieldClass_Supports 编译开关。在写法上略有差别:
procedure StringYieldProc(YieldObj: PMeYieldObject);
var
YieldValue: string;
i: integer;
begin
YieldValue:='None';
YieldObj.Yield(YieldValue);
for i := 1 to 10 do
begin
YieldValue := YieldValue + IntToStr(i);
YieldObj.Yield(YieldValue);
end;
end;
function GetEnumerator: PYieldString;
begin
Result:= New(PYieldString, Create(StringYieldProc));
end;
//Usage
with GetEnumerator^ do
try
while MoveNext do
begin
Writeln(Current);
end;
finally
Free;
end;
使用 YieldClass_Supports 编译开关的写法
{$I MeSetting.inc}
//控制逻辑:枚举器
procedure StringYieldProc(YieldObj: {$IFDEF YieldClass_Supports}TMeYieldObject{$ELSE} PMeYieldObject{$endif});
var
YieldValue: string;
i: integer;
begin
YieldValue:='None';
YieldObj.Yield(YieldValue);//返回行为逻辑
for i := 1 to 10 do
begin
YieldValue := YieldValue + IntToStr(i);
YieldObj.Yield(YieldValue); //返回行为逻辑
end;
end;
function GetEnumerator: {$IFDEF YieldClass_Supports}TYieldString {$ELSE}PYieldString{$ENDIF};
begin
Result:= {$IFDEF YieldClass_Supports} TYieldString.Create(StringYieldProc){$ELSE}New(PYieldString, Create(StringYieldProc)){$ENDIF};
end;
{$IFDEF SUPPORTS_FOR_IN}
{$IFDEF YieldClass_Supports}
type
TMyStrings = class
public
function GetEnumerator: TYieldString;
end;
function TMyStrings.GetEnumerator: TYieldString;
begin
Result:= TYieldString.Create(StringYieldProc);
end;
var
s: string;
vStrs: TMyStrings;
{$ENDIF}
{$ENDIF}
begin
{$IFDEF SUPPORTS_FOR_IN}
{$IFDEF YieldClass_Supports}
writeln('Test For_In Yield tEnumerator:');
vStrs := TMyStrings.Create;
try
for s in vStrs do
Writeln(s);
finally
vStrs.Free;
end;
{$ENDIF}
{$ENDIF}
writeln(#13#10'Test Yield tEnumerator:');
with GetEnumerator{$IFNDEF YieldClass_Supports}^{$endif} do
try
while MoveNext do
begin
Writeln(Current);
end;
finally
Free;
end;
{$IFDEF SUPPORTS_FOR_IN}
{$IFDEF YieldClass_Supports}
s := '';
{$ENDIF}
{$ENDIF}
end.