有人能帮我理解下面这段 Ruby 代码吗?

4
我最近在Tomcat中使用JRuby运行Sinatra时遇到了Permgen内存泄漏问题。 该问题与Sinatra使用的Tilt库支持各种模板选项有关。 老代码(未包含在此处)生成了内存泄漏。 新代码(如下)没有,事实上我看到Permgen GC现在正在工作。
Ruby应该是自描述的,但我无法通过阅读它来解释这段代码。 为什么会有嵌套的类eval? 为什么定义一个方法然后解除绑定?
编译一堆模板并使它们保持可重用的代码为什么看起来如此复杂?
另外:如果有任何GitHub员工查看这个问题,请添加一些功能,允许用户在代码片段中插入问题。
(此代码摘自https://github.com/rtomayko/tilt/blob/master/lib/tilt.rb
def compile_template_method(locals)  
  source, offset = precompiled(locals)  
  offset += 5  
  method_name = "__tilt_#{Thread.current.object_id.abs}"  
  Object.class_eval <<-RUBY, eval_file, line - offset  
    #{extract_magic_comment source}  
    TOPOBJECT.class_eval do  
      def #{method_name}(locals)    
        Thread.current[:tilt_vars] = [self, locals]  
        class << self  
          this, locals = Thread.current[:tilt_vars]  
          this.instance_eval do  
            #{source}  
          end  
        end  
      end  
    end  
  RUBY  
  unbind_compiled_method(method_name)  
end  

你确定这个链接指向正确的位置吗? - Andrew Grimm
2个回答

5
有嵌套的类eval。为什么?
这个方法看起来不像是优雅的自描述代码,而更像是经过战斗、修补和固定的生产代码(所以也许我们可以原谅他们一点)。
那么为什么有两个evals?在第二个嵌套的“真正”模板方法代码可以被评估之前,要评估的代码必须用正确的源编码前缀进行标记,该编码可能已在模板文件中定义为“魔法注释”。
一旦字符串编码设置正确,就可以尝试真正的class_eval了。另一种说法是“这是编写源代码的源代码的源代码”!
大概是为了解决兼容性问题,在Ruby 1.9中可能会出现这样的问题:正在编译的模板可能包含一个字符编码(UTF-8),该编码与Tilt库源代码本身的编码(US-ASCII编码)不同,这将导致模板字符串的错误评估(因为字符串编码已在调用模板文件的主机代码中设置)。
为什么要定义一个方法,然后解除绑定?
为了澄清:在Ruby中,“未绑定(unbound)”并不等同于“未定义(undefined)”。
未绑定方法是自由的方法对象,属于UnboundMethod类型,可以被调用,但它们不再与特定对象相关联。未绑定方法不再具有接收者。
为了创建一个未绑定方法,首先必须将其绑定到(针对)一个对象。这就是为什么编译模板方法会很快从顶级对象中移除,因为它只是一种临时安排,用于生成未绑定方法。
这种技术可用于使得编译模板可以针对给定类的不同实例进行作用域控制,而无需以任何可见或永久的方式更改根对象或第三方开发人员的客户端类。
通过取消编译模板方法与特定客户端代码对象的关联,编译模板方法可以在将来调用使用该类型对象的模板时重新绑定到该对象的类的新实例上。
例如,给定以下ERB模板:
<p>Hello <%= @name %></p>

...以及以下调用代码:

scott = Person.new
scott.name = "Scott"
output = template.render(scott)
=> "<p>Hello Scott</p>"

在第一次渲染期间,模板会被评估并编译为 TOPOBJECT 对象的实例。编译后的模板方法将被命名为“__tilt_2151955260”。然后该方法将解除绑定以便于对所有类型为 TOPOBJECT 的实例(取决于 Ruby 版本,可能是 Object 或 BasicObject)再次使用,因此可以针对任何客户端对象类型使用。
下一次渲染模板时,编译后的模板方法将与 TOPOBJECT 的 'baq' 实例进行绑定:
baq = Person.new
baq.name = "Baq"
output = template.render(baq)

在幕后,当调用template.render(baq)时,未绑定的编译模板方法将被绑定到Person类的'baq'实例上:
__tilt_2151955260.bind(baq).call

不必每次调用class_eval可以大大提高性能。
引用块: 为什么编译一堆模板并保留它们以供重复使用的代码看起来如此复杂?
我的评估是,尽管代码实现在第一眼看起来确实过于复杂,但这些间接层经常在框架代码中是必要的,该代码旨在使公共API对于成千上万其他开发者非常简单和易于消费,即使是以维护它为代价的少数开发者。
由于被许多不同区域使用的API(因此许多来自世界各地的编码方式),代码复杂性(双倍嵌套)也增加了。
脚注: 问题中提到的Template类已经被重构为一个单独的文件 github.com/rtomayko/tilt/blob/master/lib/tilt/template.rb

Scott,感谢您这么详尽和提升的回复!跟进一下观察:我注意到模板方法名称是使用当前线程的对象ID定义的。至少在JRuby世界中,这使得事情是线程安全的,但会增加开销,因为相同的模板+本地变量将为请求线程池中的所有线程评估。遗憾。再次感谢! - Baq Haidri
哦,又有一个后续问题: - Baq Haidri
糟糕!再试一次:哦,又有一个跟进的问题:为什么Ruby 1.9区分BasicObject和Object?当使用Ruby 1.9评估上面的代码片段时,顶层class_eval在Object中进行,而嵌套的class_eval在BasicObject上进行。有任何想法吗? - Baq Haidri
确实。那个模板命名策略是一个非常相关的观察。但总体来说,这是在性能和潜在复杂性方面可以接受的权衡。 - Scott
是的,我也有同样的想法,但我不会把自己描述成 Ruby 忍者;我们不应该排除我们可能都(吸气!)错过了什么的可能性。 - Scott
显示剩余2条评论

1

我对这段代码的理解如下:

Object.class_eval将执行当前范围之外的第一个块,并在全局范围内执行(如果出现错误,则eval_file和line-offset只是用于打印正确的行号和文件名),然后将在虚拟容器中创建一个新方法(我假设这里是TOPOBJECT),一旦方法编译完成,它就会被解除绑定并存储在其他地方。

之后,该方法将附加到包含模板变量并在其中运行的新对象上,我不记得确切的语法,但这是个想法(其中方法是未绑定的方法):

object = SomeClass.new
object.param1 = "something"
object.param2 = 43
method.apply(object)

就代码复杂性而言,我已经不得不写出类似的东西(虽然没有那么复杂),以使上述api易于使用,这有时就是代价。

据我所知,未绑定方法存储在@compiled_method中,这是在compiled_method方法内完成的(我没有看到任何其他调用compile_template_method的内容)。 - mu is too short

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接