Ruby 解决 stack level too deep 问题

runup · 2015年12月26日 · 最后由 hanji 回复于 2016年01月20日 · 6034 次阅读
class Computer
  def mouse
    component :mouse
  end

  def component(name)
    puts "get the #{name} information in the computer"
  end
end

cp = Computer.new
cp.mouse

代码运行成功,但是改动其中的代码:

def mouse
   component :mouse
end

改为

def mouse
   component(mouse)
end

出现了 stack level too deep (SystemStackError),百度了下是死循环,为什么会出现这个问题?

上面的问题已经解决,楼盖了这么高,主要是通过这个问题又引出了其他问题,引申的问题如下所示

经过楼下楼下伙伴的提醒,解释器对每个表达式都是从右向左求值,在上面的代码中,component :mouse 中参数是 symbol 对象,component 方法可以将这个已经定义的对象作为参数代入求值,而在 component mouse 这段代码中,解释器会寻找 mouse 这个变量,找到程序已经定义了这个变量作为一个方法名,因此在执行这段程序的时候,会引出如上的 SystemStackError 错误。按照解释器对每个表达式都是从右向左求值这个简单道理,如下的代码会出现 undefined variable or method 错误,代码如下所示:

def mouse 
  component(dog)
end

因为在解释器无法找到 dog 这个变量的定义。 总结来说,当方法的参数是已经定义的对象,比如 Symbol(:mouse)对象和 String("mouse") 对象,可以按照预期使用这些对象参数,但是如果方法的参数是变量的形式,比如 mouse 的形式,解释器会查找这个变量是否已经被定义,如果被定义成方法,则会当做方法进行调用。这些都是我自己的理解,但是如下的代码却和我理解有些矛盾,代码实现了send方法的动态调用,具体代码如下所示:

class MyClass
  def my_method(my_arg)
    puts my_arg*2
  end
end

obj = MyClass.new
obj.send(:my_method, 3)  #=>6
obj.send("my_method", 6) #=>6
obj.send(my_method, 6) #=> undefined variable or method error

在上面的代码中,:my_method 和“my_method”被当做是方法名传递给了对象 obj,而本身是 symbol 对象和 String 对象,而 my_method 变量在上面已经进行了定义,它是一个方法,在如上的代码中如果执行会报出 undefined variable or method error,而这刚好和上面最初的理解相矛盾。为什么会这样?还是说这是send方法特有的形式?

总结楼下的答案: 1、

obj.send(my_method, 6) #=> undefined variable or method error

报错的原因是 obj.send(my_method, 6) ,这个作用域是 main 对象的作用域,而 MyClass 类中的方法 my_method 不在 main 对象作用域内,因此 send 使用这个 my_method 这个参数的时候会报错,因为在 main 对象的作用域内的,my_method 方法并没有定义。 2、

obj.send(:my_method, 3)  #=>6
obj.send("my_method", 6) #=>6

至于说 send 方法能将字符串对象和符号对象接受为方法,send文档本身就是这么定义,该方法只接受符号对象和字符串对象作为参数,可以认为这是 Object 的方法 send 约定的特有的形式。

经过楼下伙伴的提醒,send 从广义上说,接受的参数只要能返回字符串对象和符号对象,那么这个参数就是可行的,见如下的代码所示:

class MyClass
  def my_method(my_arg)
    puts my_arg*2
  end
end

obj = MyClass.new

def simulate
  :my_method
end
obj.send(simulate, 3)  #=>6

component(mouse)

component(component(mouse))

component(component(component(mouse)))

...

#1 楼 @rei 那相当于 componet :mouse 是发送参数 mouse 给 componet 方法,而 componet(mouse) 是将方法传递给 componet,因此造成死循环?

#2 楼 @runup :mouse 是符号,mouse 是方法。

#3 楼 @rei 我将

def mouse
  component(mouse)
end

其中的 mouse 改为任意一个变量 cc,即代码如下:

def mouse
  component(cc)
end

出现了问题为:undefied local variable and method "cc",但是代码改为 component(:cc),则出现正常,那么我想问,在 componet(XX),中,如何判断 XX 是参数名还是方法名?

看元编程之前看了基本语法书了没?

6 楼 已删除

#4 楼 @runup

建议楼主看看这篇文章 http://www.ibm.com/developerworks/cn/opensource/os-cn-rubysbl/

加了冒号相当于创建了一个 symbol 对象,所以传递的参数就是这个 symbol 对象,解释器不会认为它是一个方法或者变量,就好像你使用 componet("string"),componet(1) 是一个概念。

由于 Symbol 比 String 在执行时更为高效,所以在一些仅仅只需要表示"名称"的场合,Rubyist 都会倾向于使用 symbol。例如这个 hash{"name" => "adam"},String 当然也可以用来作为 key,但几乎没有人这样用,因为 Ruby 在处理 Symbol 时更加快速,占用的资源更少。许多方法都以 Symbol 而不是 String 来作为名称参数,比如 define_method(symbol, method) 。在 Rails 里,绝大多数 DSL 后面的参数,跟的也是 Symbol。虽然 Rails 有自己的补救方法可以兼容 Symbol 和 String,但最好还是使用 Symbol 来表示名称。

#7 楼 @adamshen 谢谢指点,链接的内容已经看完,并且收藏。我的理解是:

def mouse
   component :mouse
end

若对象调用了 mouse 方法,就是在这个方法中执行 component 方法,这个方法的参数是 mouse。但是为什么将:mouse 的符号标识符去掉,变为 mouse 之后,如下代码:

def mouse
   component mouse
   #componet(mouse) 与上面代码功能相同
end

就变成了死循环,为什么这个 mouse 不能作为 component 方法的一个参数使用呢?

#8 楼 @runup 在 mouse 方法内部调用自己本身,这是递归调用,所以必须要有退出这个递归的条件,不然就是死循环。

至于 stack level too deep (SystemStackError),每次调用一个子方法的时候,都会将原方法的地址及局部变量入栈,以方便返回。如果不断递归调用本身,栈内存区域迟早会被耗完,所以就报错了。

#8 楼 @runup mouse 可以作为 component 的参数,可是 mouse 是一个方法,在传入之前解释器要求他的值,也就是执行这个方法

比如

component  1+1

在传入 component 之前会对 1+1 求值,也就说传入的是 2

#9 楼 @adamshen 递归的原理这个我明白,只是不明白为什么 mouse 是作为方法在使用,而不是作为 componet 方法的参数变量在使用。

def mouse
   component mouse
   #componet(mouse) 与上面代码功能相同
end
12 楼 已删除

#11 楼 @runup ruby 的方法调用是不用加括号的所以mousemouse()是一样的

#11 楼 @runup 这个和 ruby 解释器的原理有关,具体的步骤不太清楚,但是可以根据一般的编译原理想一下。一旦切换了 scope,当前 scope 下的符号表就会更新。符号表里会记录当前 scope 下面的变量名和方法名,只要代码里出现符号表里可以找到的单词,就解释为变量和方法,反之,就会提示找不到这个方法或者变量。

#10 楼 @kikyous 如果将代码中的 component mouse 改为 component cc,则显示的是 undefined local variable or method, 可见,这边的“cc”,没有作为参数在被使用,但是改为 component :cc ,则代码执行成功,这说明:cc 在作为参数被使用,这是为何?

#15 楼 @runup 解释器对每个表达式都是从右向左求值的的,cc 既不是已定义的变量,也不是方法,所以求值会报错

:cc 是一个符号类型的值,所以求值(求值的结果就是:cc)通过,然后作为参数传入 component 方法

所以 cc 在传入 component 之前就已经发生错误了

#16 楼 @kikyous

解释器对每个表达式都是从右向左求值的的

谢谢指点,我不是专业出生,编译原理和解释器的知识基本上没有,按照这个说法,那么能解释的通了,感谢。

#17 楼 @runup 推荐你看 计算机程序的构造和解释(Structures and Interpretations of Computer Programs) 这本书

#18 楼 @kikyous 前段时间买了,还没有开始看,感谢。

不太明白为什么回答得如此复杂。

:mouse 是 symbol, 没有任何意义的一个变量,注意嘴型,这个是一个变量。 mouse 是方法名,是方法名

def mouse compute mouse end 就是递归调用 mouse 这个方法。 没有退出条件,自然就是死循环。

#20 楼 @leiz_me :mouse 应该叫是叫符号对象

#21 楼 @runup 呃,我的错,习惯了这种说法。那就来个学究点的。

A Ruby symbol is a thing that has both a number (integer) representation and a string representation.

from http://www.troubleshooters.com/codecorn/ruby/symbols.htm

只是一种表现,要怎么理解都可以。关键是,在这个场景里,把它传到方法里和把一个变量传到方法里没区别,或者说是旗标的意义会更符合实际的情况。

#22 楼 @leiz_me 我的理解是传入 mouse,这是个变量,假如这个变量被定义过是一个方法,那么解释器会认定该变量是一个方法名称,出现文中的 SystemStackError 错误,但是如果该变量没有定义过,则会报出 undefined 错误。如果传入的是:mouse,解释器会认为这个一个已经被定义过的 Symbol 对象,可以作为参数被使用。

#23 楼 @runup mouse 不是变量,这个只是 mouse() 的另一种写法而已。

#24 楼 @jpman 恩,如果代码中没有定义过 mouse 这个方法,那么解释器会认为它是一个变量,如果已经定义过 mouse 这个方法,那么解释器会认为它是一个方法,和 mouse() 方法的简写形式。

#16 楼 @rei @kikyous @kikyous 看书的时候遇到下面的代码,似乎和我对问题的理解以及您的解释有点相悖,代码如下:

class MyClass
  def my_method(my_arg)
    puts my_arg*2
  end
end

obj = MyClass.new
obj.send(:my_method, 3)  #=>6
obj.send("my_method", 6) #=>6
obj.send(my_method, 6) #=> undefined variable or method error

问题中将 mouse 当做是方法,把:mouse 当做是一个符号对象,而这里却刚好相反。 还是说send的这种方式是一种约定?

#26 楼 @runup 我看不到哪里相反了

#27 楼 @kikyous 在本文的问题中,:mouse 被当做是符号对象来处理,mouse 被当做方法来处理。但是在 26 楼的代码例子中,:my_method 被当做是方法来处理,my_method 被当做变量来处理。

#28 楼 @runup :my_method 和 "my_method" 以及 my_method 都是 obj.send 的参数,不同的是前两个一个是 symbol 一个是 string,第三个则需要在程序的上下文查找具体的值,那么这里 ruby 首先会查找有没有相应的变量或者方法。

#29 楼 @imconfused 前两个一个是 symbol,一个是 string,为什么不认为它是对象,而把它当做是方法名?

#30 楼 @runup Ruby 中所有的都是对象。因为 :my_method "my_method" my_method 这几个只有第三个没有具体的值 要不你再试试,你仔细看看 send 方法是做什么的

class MyClass
  def my_method(my_arg)
    puts my_arg*2
  end
end

obj = MyClass.new
obj.send(:my_method, 3)
obj.send("my_method", 6)

my_method = "my_method"
obj.send(my_method, 6) 

my_method = :my_method
obj.send(my_method, 6) 
# 省略括号版本
def mouse
  component mouse
end

等效于

# 带括号版本
def mouse()
   component(mouse())
end
# 更明显版本(注意并不完全等价)
def mouse()
   component(self.mouse())
end

里面的 mouse 既不是 String, 也不是方法名,而是一次方法调用

楼主有那么难理解吗,给你看看你这个代码的 backtrace:

(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
#...... 很多很多个 mouse 调用
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):24:in `mouse'
(pry):48:in `__pry__'
/Users/martin/.rvm/gems/ruby-2.2.2/gems/pry-0.10.3/lib/pry/pry_instance.rb:355:in `eval'
/Users/martin/.rvm/gems/ruby-2.2.2/gems/pry-0.10.3/lib/pry/pry_instance.rb:355:in `evaluate_ruby'
/Users/martin/.rvm/gems/ruby-2.2.2/gems/pry-0.10.3/lib/pry/pry_instance.rb:323:in `handle_line'
/Users/martin/.rvm/gems/ruby-2.2.2/gems/pry-0.10.3/lib/pry/pry_instance.rb:243:in `block (2 levels) in eval'
/Users/martin/.rvm/gems/ruby-2.2.2/gems/pry-0.10.3/lib/pry/pry_instance.rb:242:in `catch'
/Users/martin/.rvm/gems/ruby-2.2.2/gems/pry-0.10.3/lib/pry/pry_instance.rb:242:in `block in eval'

简单这么说吧,就是:

我是谁?我是我!但我是谁?我是我啊!我是谁?我是我啊!... (过了好久)溢出了~~~

#33 楼 @martin91 楼上显示不出来啊

#34 楼 @runup 已更新,刚才手快了。你的问题就是因为出现了无终止条件的递归。

#35 楼 @martin91 感谢。递归的问题我已经理解了,楼盖的这么高,主要是引出了其他问题。能不能帮我看一下 26 楼的代码?感谢。

#36 楼 @runup :cc 就是一个,跟你其他什么 1,2,3,5 这些数值以及 "hello" 这样的字符串本质一样,都是 字面值,是没有所谓的求值过程的,而如果你用了 cc ,那从语法上,就不是字面值,那解释器就认为是变量名或者方法名了,所以解释器尝试从上下文中找到这个变量或者方法的定义,遗憾的是,找不到。。。所以错了。

#16 楼 @kikyous 你前面说的没错,后面就不敢苟同了,值本身是不需要求值的。 :cc 就是一个值,就是一个固定的值,没有甚么需要求值的过程。

#36 楼 @runup 不知道 字面值 所云何物的话,可以看看 Ruby 参考手册 - 字面值,末尾就有介绍符号类型的字面值

#39 楼 @martin91 谢谢指点,我的最初的问题已经更新,能不能帮我看下~感谢。

object 的 send 本身的实现就只接受 symbol 和字符串。这个看看文档即可。追究为何如此,好像倒有点钻牛角尖了。当然你不妨看 send 的源码. 至于说为何是 undefined 错误,我觉得是上下文的关系,obj.send(my_method, 6) 中的 my_method, 是 main 中的 my_method, 而你并未定义.(你的 my_method 是 MyClass 中的,外部不可见). 你如果 obj.send( obj.my_method, 6) 自然就可以找到了,然而并没有什么用。send 只接受 symbol 和字符串。

#41 楼 @chenjau bingo,这个答案我能理解,感谢。

#42 楼 @runup 不要死抠文档

至于说 send 方法能将字符串对象和符号对象接受为方法,send 文档本身就是这么定义,该方法只接受符号对象和字符串对象作为参数,可以认为这是 Object 的方法 send 约定的特有的形式。

只要你传入的参数能够返回 Symbol 对象或者 String 对象就行了,比如:

# 传入这两种类型的字面量
obj.send : my_method
obj.send "my_method"

# 方法返回 Symbol 类型的值
def my_method
  : my_method
end
obj.send my_method

这些都是可以的。

关于前面的上下文的问题,建议你去阅读《Ruby 元编程》,里边有关于祖先链以及方法查找的详细例子跟解释。

#43 楼 @martin91 涨知识了,已经把知识补充到帖子中了,感谢。

点个赞

需要 登录 后方可回复, 如果你还没有账号请 注册新账号