论坛首页 编程语言技术论坛

疑问:yield到底是怎么运作的?

浏览 31021 次
该帖已经被评为精华帖
作者 正文
   发表时间:2006-10-31  
  最早看到yield时,就用c/C++中的概念对比了一番,发现最接近就是“宏代码的展开”。粗看起来,这样的理解是可以的。不过马上就有个问题出来了,就是定义域。宏代码的展开,要求展开后的代码处于被展开的位置同一个定义域,否则,相关变量和函数就会出现没有定义的错误。 不过从下面的ruby代码来看,yield没有这样的问题存在

class A
def self.test
yield
end
end
class B
def test0
puts "call from class B"
end
def test1
A.test {test0}
end
end
b = B.new
b.test1


如果以代码展开的角度来理解上面代码的运行,那么就应该出现test0 不是A的函数这样的错误提示。不过运行下来很成功,没有出现任何错误。

还有一种可能是ruby的函数调用,就像C++一样,默认附带一个this对象。

def test1
A.test {test0}
end


这段代码在执行时是不是被修改成了

def test1
A.test {@this.test0}
end


ruby如果真有这个功能,上面代码的执行似乎能够说的通。

不过上面都是猜测,看下《Programming Ruby - 2nd》第一次提到yield的时候得说明
引用
First, a block may appear only in the source adjacent to a method call; the block is written starting on the same line as the method call’s last parameter (or the closing parenthesis of the parameter list). Second, the code in the block is not executed at the time it is encountered. Instead, Ruby remembers the context in which the block appears (the local variables, the current object, and so on) and then enters the method. This is where the magic starts.
Within the method, the block may be invoked, almost as if it were a method itself, using the yield statement. Whenever a yield is executed, it invokes the code in the block.When the block exits, control picks back up immediately after the yield.1 Let’s start
with a trivial example.

def three_times
yield
yield
yield
end
three_times { puts "Hello" }
produces:
Hello
Hello
Hello


这段文字提到了3点。
第一,保存block相关的context
第二,block调用像函数本身的一部分
第三,有个神秘的control在操纵这一切
如果第一点,可以看作ruby给test0附加了个this对象,但是第二第三个就说不通了,如果只是代码展开,第二第三就不需要说明了。看来关于yield是代码展开的想法是完全错的。

那么从第三点来看,yield的确有个代码运行块(先这样叫着)切换的动作,最后运行完了又切换回来.
加入代码切换的说法之后,给出的例子代码似乎是这样运行的

1.运行A.test的上半部分直到遇到yield
2.写入block的context,同时使用block的context和A.test运行到一半的context,作为block的运行环境来运行block代码,
3.block运行完毕,清除block的context,然后继续运行A.test的下半部分。

古怪阿古怪,yield是真的这样运行的吗?

从前面yield的翻译讨论来看,yield带有切换,退让,让步这类的意思。
yield如果有执行代码切换的功能,多少带有超级微型线程的意思了。而且是主动切换,执行代码主体。yield完全是一个控制代码执行顺序的关键字了。真神奇。

   发表时间:2006-10-31  
想得太复杂了吧,yield应该是简化block语法用的:
class A
  def self.test(&block)
    block.call
  end
end


class A
  def self.test
    yield
  end
end

是一样的。这样看不是很明白了嘛。

你可以把省略的&block参数理解为你说的隐藏的this,但本质上和c++的this有区别的,ruby的self才和c++的this相似。
1 请登录后投票
   发表时间:2006-10-31  
qiezi 写道


你可以把省略的&block参数理解为你说的隐藏的this,但本质上和c++的this有区别的,ruby的self才和c++的this相似。


那么block中定义在class B中的函数test0,运行时,是怎么找到test0的函数体,然后正确调用的呢?
0 请登录后投票
   发表时间:2006-10-31  
test0和test1在一个类里,test1当然知道test0。 你如果在test0里puts self就会看见输出的是B的实例。所以yield运行时动态切换context的,不是宏那样静态替换掉的。

BTW, 你竟然用了code框却不缩进?哼哼。另外有个手误:putc
0 请登录后投票
   发表时间:2006-10-31  
cookoo 写道
test0和test1在一个类里,test1当然知道test0。 你如果在test0里puts self就会看见输出的是B的实例。所以yield运行时动态切换context的,不是宏那样静态替换掉的。

BTW, 你竟然用了code框却不缩进?哼哼。另外有个手误:putc

哈哈,看到你这样说,看来最后我的理解应该没有错.关键还是在于切换context. block中的代码才能顺利运行.
还真像thread的切换,不过这里的切换是主动引起的.

手误已改 谢谢.


0 请登录后投票
   发表时间:2006-10-31  
context切换是肯定要的,因为反过来就不自然嘛,你写的代码你根本不知道运行在何种context下,你得去猜测那种context下有些什么东东。

不过这里是用例子证明了这一样,但我还是愿意反过来想:如果不切换context,那么block所处的context将是不自然的、不完整的,所以ruby开发者设计成需要context切换。。。似乎所有语言中类似的玩意都是这么设计的,可以想像成其它语言的委托。
0 请登录后投票
   发表时间:2006-10-31  
qiezi 写道
context切换是肯定要的,因为反过来就不自然嘛,你写的代码你根本不知道运行在何种context下,你得去猜测那种context下有些什么东东。

不过这里是用例子证明了这一样,但我还是愿意反过来想:如果不切换context,那么block所处的context将是不自然的、不完整的,所以ruby开发者设计成需要context切换。。。似乎所有语言中类似的玩意都是这么设计的,可以想像成其它语言的委托。


所以,有人用yield也能实现类似微线程之类的东西。
0 请登录后投票
   发表时间:2006-10-31  
qiezi 写道
context切换是肯定要的,因为反过来就不自然嘛,你写的代码你根本不知道运行在何种context下,你得去猜测那种context下有些什么东东。

不过这里是用例子证明了这一样,但我还是愿意反过来想:如果不切换context,那么block所处的context将是不自然的、不完整的,所以ruby开发者设计成需要context切换。。。似乎所有语言中类似的玩意都是这么设计的,可以想像成其它语言的委托。


所以yield就是一个说明什么时候切换context的关键字,call block那是接下来的事情了.
0 请登录后投票
   发表时间:2006-10-31  
charon 写道
qiezi 写道
context切换是肯定要的,因为反过来就不自然嘛,你写的代码你根本不知道运行在何种context下,你得去猜测那种context下有些什么东东。

不过这里是用例子证明了这一样,但我还是愿意反过来想:如果不切换context,那么block所处的context将是不自然的、不完整的,所以ruby开发者设计成需要context切换。。。似乎所有语言中类似的玩意都是这么设计的,可以想像成其它语言的委托。


所以,有人用yield也能实现类似微线程之类的东西。


超微线程,yield的确有这样的含义存在,和普通线程切换不同,就是一个主动,一个被动切换。能够用yield完成这个,的确也够强的。
0 请登录后投票
   发表时间:2006-10-31  
看到那个“yield是占位符”的帖子我就有些疑惑了。
在我的印象中,Ruby Yield, Python Yield, C# Yield 都是用来实现 coroutine的。都可以用来实现 iterator, generator。既然是 coroutine,自然是 context switch。

不过,我还记得 cookoo 的 python vs ruby 帖子,里面说,ruby yield 和 python yield 不是一回事,python yield 是用来产生 generator的。

看了这个帖子上面的讨论,发现所有语言的yield 原来还都是一回事。都是用来表示 coroutine。
想想也是。yield 这个词也不是随便用的。都已经约定俗成表示coroutine了。

-------------------------

yield的大致含义:

yield 基本等于 return。

用在一串顺序指令中间。

yield 1;
yield 2;
yield 3;

需要注意的是,
return 1;
return 2;
return 3;

return这种写法是无法通过的。
yield 就可以。因为 yield 允许在顺序指令中主动中断。
yield 还同时保存了当前的 runtime stack & context。所以,是可以重入的。下次context切换回来,stack/context 就恢复了。可以继续执行。

yield 的用途就是用来 产生序列,一串顺序的yield可以构造一个发生器/generator/ iterator。

所以,在发生器方面,ruby yield 和 python yield / c# yield 的含义是相同的。

所不同的方面是接受器方面。ruby yield 的接受器方面要求一个 block 来接受 yield 产生的每一步序列。

这是一个 iterator lazy fetch 的过程。

receiver  执行 next, 切换到 generator。
generator 执行 yield 切换到 receiver。
这样一步一步合作执行。

关于 coroutine, yield。参见这个帖子的回复
http://www.iteye.com/topic/21293

buaawhl 写道

关于coroutine, yield, iterator, ajoo有些文章和代码可以作为很好的参考。

http://jroller.com/page/ajoo?entry=yield_support_in_java
这个blog讲述了基本思想。

http://www.sourceforge.net/projects/jfunctional
这个项目的代码包括了一个 yield package,是 yield coroutine iterator的实现。

-------

另外, c#支持coroutine yield iterator。
我找到了一篇CLR关于这方面支持的ppt文档,有兴趣可以参阅。

http://research.microsoft.com/workshops/SSCLI2005/presentations/Ierusalimschy-Mascarenhas.ppt


还有原贴

buaawhl 写道

聪明的人们把目光转向了Coroutine。
Coroutine本来是一个通用的概念。表示几个协同工作的程序。
比如,消费者/生产者,你走几步,我走几步;下棋对弈,你一步我一步。
由于协同工作的程序通常只有2个,而且这两个程序交换的数据通常只有一个。于是人们就很容易想到用Coroutine来实现Iterator。
这里面Iterator就是Coroutine里面的生产者Producer角色,数据提供者。所以,也叫做Generator。
每次Iterator程序就是等在那里,一旦用户(消费者Consumer角色)调用了iterator.next, Iterator就继续向下执行一步,然后把当前遇到的内部数据的Node放到一个消费者用户能够看到的公用的缓冲区(比如,直接放到消费者线程栈里面的局部变量)里面,然后自己就停下来(wait)。然后消费者用户就从缓冲区里面获得了那个Node。
这样Iterator就可以自顾自地进行递归运算,不需要自己管理一个栈,而是迫使计算机帮助它分配和管理运行栈。于是就实现了幸福得像花儿一样,简单得像Visitor一样的梦想。

比如,这样一段代码,遍历一棵二叉树。
public class TreeWalker : Coroutine {
private TreeNode _tree;
public TreeWalker(TreeNode tree) { _tree = tree; }
protected override Execute() {
Walk(_tree);
}
private void Walk(TreeNode tree) {
if (tree != null) {
Walk(tree.Left);
Yield(tree);
Walk(tree.Right);
}
}
}

其中的Yield指令是关键。意思是,首先把当前Node甩到用户的数据空间,然后自己暂停运行(类似于java的thread yield方法或者object.wait方法),把自己当前运行的线程挂起来,这样虚拟机就为自己保存了当前的运行栈(context)。
有人说,这不就是continuation吗?
对。只是Coroutine这里多了一个产生并传递数据的动作。
实现原理如果用Java Thread表示大概就是这样。当然下面的代码只是一个示意。网上有具体的Java Coroutine实现,具体代码我也没有看过,想来实现原理大致如此。

public class TreeIterator implements Iterator{
TreeWalker walker;
// start the walker thread ..
Object next(){
walker.notify();
// wait for a while so that walker can continue run
return walker.currentNode;
}
}

class TreeWalker implements Runnable{
TreeNode currentNode;

TreeWarker(root){
currentNode = root;
} void run(){
walk(curentNode);
}

private void Walk(TreeNode tree) {
if (tree != null) {
Walk(tree.Left);
currentNode = tree;
this.wait();
Walk(tree.Right);
}
}
}

我们看到,Iterator本身是一个Thread,用户也是一个Thread。Iterator Thread把数据传递给User Thread。
说实话,我宁可自己维护一个Stack,也不愿意引入Coroutine这类Thread Control的方式来实现Iterator。
总结

0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics