Ruby method_missing,一个 Ruby 程序员的梦中情人

dotnil · 2012年05月22日 · 最后由 hiveer 回复于 2018年03月29日 · 13817 次阅读

小感冒,晕乎乎,翻译一篇文章攒人品,望自己明天生龙活虎

地址:http://www.alfajango.com/blog/method_missing-a-rubyists-beautiful-mistress/ 标题:method_missing: A Rubyist’s Beautiful Mistress 译者:逸才

我最近读了些文章(比如这篇),宣传在 Ruby 里使用 method_missing 的。

很多人都与 method_missing 干柴烈火,但在并没有小心处理彼此之间的关系。所以,我想来探讨一下这个问题:

** 我该怎么用 method_missing **

什么时候该抵挡 method_missing 的诱惑

首先,永远不要在还没花时间考虑你用得够不够好之前,就向 method_missing 的魅力屈服。你知道,在日常生活中,很少会让你以为的那样亟需 method_missing

日常:方法代理

案例:我需要让这个类能够使用另一个类的方法

这是我所见过最普遍的使用 method_missing 的情况。这在 gems 与 Rails 插件里头尤其流行。它的模型类似这样:

class A def hi puts "Hi from #{self.class}" end end

class B def initialize @b = A.new end

def method_missing(method_name, *args, &block) @b.send(method_name, *args, &block) end end

A.new.hi #=> Hi from A B.new.hi #=> Hi from A

如此,B 就拥有了 A 的所有实例方法。但是让我们想想,在调用 @b.hi 的时候都发生了什么。你的 ruby 环境沿着继承链一路找 hi 这个方法,到最后,恰恰在丢出个 NoMethodError 前,它调了 method_missing 这个方法。

在上例中,情况并不坏,毕竟这里就两个微不足道的类需要查。但通常,我们是在 Rails 或者其他一些框架的上下文中编程。而你的 Rails 模型继承自 ActiveRecord,而它又集成自其他一大坨的类,于是现在你就有了一坨高高的堆栈要爬⋯⋯ 在你每次调用 @b.hi 的时候!

你的好基友:define_method

估计现在你在抱怨,“但是史蒂夫,我需要 method_missing” 我告诉你,别忘了其实除了情妇之外,你还有个忠诚的好基友,叫做 define_method

它允许你动态地定义一个方法(顾名思义)。它的伟大之处在于,在它执行过之后(通常在你的类们加载之后),这些方法就存在你的类中了,简单直接。在你创建这些方法的时候,也没有什么继承链需要爬。

define_method 很有爱很可靠,并且能够满足你的日常生活。不信我?接着看⋯⋯

class B define_method(:hi) do @b.hi end end

“可是我有一大坨方法要定义!” 你抱怨

“没问题!” 我卖萌眨眼

class B [:hi, :bye, :achoo, :gesundheit].each do |name| define_method(name) do @b.send(name) end end end

可是我懒得把它们一个个写出来!

你有点难搞哦

class A # ... lots of methods in here end class B A.instance_methods.each do |name| define_method(name) do @b.send(name) end end end

那假如我要定义的方法跟原本的有那么一些些不一样呢?

容易

class A def hi puts "Hi." end end

class B A.instance_methods.each do |name| define_method("what_is_#{name}") do if @b.respond_to?(name) @b.send(name) else false end end end end

B.new.what_is_hi #=> "Hi." B.new.what_is_wtf #=> false

呃,代码看起来不优雅啊

那就没办法了,凑合得了。如果你想要代码更易读,可以看看我们的 ruby delegation libraryRails ActiveRecord delegation

好,我们总结一下,看看 define_method 的真正威力。

修改自 ruby-doc.org 上的 例子

class A def fred puts "In Fred" end def create_method(name, &block) self.class.send(:define_method, name, &block) end define_method(:wilma) { puts "Charge it!" } end class B < A define_method(:barney, instance_method(:fred)) end

a = B.new a.barney #=> In Fred a.wilma #=> Charge it! a.create_method(:betty) { p self.to_s } a.betty #=> B

什么时候用 method_missing

现在你估计在想,总有该用它的时候吧,不然还要它干嘛?没错。

动态命名的方法(又名,元方法)

案例:我要依据某种模式提供一组方法。这些方法做的事情顾名思义。我可能从来没有调用过这些可能的方法,但是等我要用的时候,它们必须可用。

现在才是人话!这其实正是 ActiveRecord 所采用的方式,为你提供那些基于属性的动态构建的查找方法,比如 find_by_login_and_email(user_login, user_email)

def method_missing(method_id, *arguments, &block) if match = DynamicFinderMatch.match(method_id) attribute_names = match.attribute_names super unless all_attributes_exists?(attribute_names) if match.finder? # ...you get the point end # my OCD makes me unable to omit this # ... else super # this is important, I'll tell you why in a second end end

权衡利弊

当你有一大堆元方法要定义,又不一定用得到的时候,method_missing 是个完美的折衷。

想想 ActiveRecord 中基于属性的查找方法。要用 define_method 从头到脚定义这些方法,ActiveRecord 需要检查每个模型的表中所有的字段,并为每个可能的字段组合方式都定义方法。

find_by_email find_by_login find_by_name find_by_id find_by_email_and_login find_by_email_and_login_and_name find_by_email_and_name # ...

假如你的模型有 10 个字段,那就是 10!(362880)个查找方法需要定义。想象一下,在你的 Rails 项目跑起来的时候,有这么多个方法需要一次定义掉,而 ruby 环境还得把它们都放在内存里头。

老虎·伍兹都做不来的事情。

** 正确的 method_missing 使用方式

(译者猥琐地注:要回家了,以下简要摘译)

1、先检查

并不是每次调用都要处理的,你应该先检查一下这次调用是否符合你需要添加的元方法的模式:

def method_missing(method_id, *arguments, &block) if method_id.to_s =~ /^what_is_[\w]+/ # do your thing end end

2、包起来

检查好了,确实要处理的,请记得把函数体包在你的好基友,define_method 里面。如此,下次就不用找情妇了:

def method_missing(method_id, *arguments, &block) if method_id.to_s =~ /^what_is_[\w]+/ self.class.send :define_method, method_id do # do your thing end self.send(method_id) end end

3、擦屁股

自己处理不来的方法,可能父类有办法,所以 super 一下:

def method_missing(method_id, *arguments, &block) if method_id.to_s =~ /^what_is_[\w]+/ self.class.send :define_method, method_id do # do your thing end self.send(method_id) else super end end

4、昭告天下

def respond_to?(method_id, include_private = false) if method_id.to_s =~ /^what_is_[\w]+/ true else super end end

要告诉别人,你的类虽然暂时还没有这个方法,但是其实是能够响应这方法的。

** 总结 **

在每个 Ruby 程序员的生活中,这仨方法扮演了重要的角色。define_method 是你的好基友,method_missing 是个如胶似漆但也需相敬如宾的情妇,而 respond_to? 则是你的爱子,如此无虞。

用 method_missing 定义的方法是实例方法吧,如果我要在 Module 里,需要定义一系列的类方法呢?

楼主译得好文章阿,赞一个

#1 楼 @yzhrain 你的问题似乎没什么特别的阿,模块被 included 的时候有回调函数,参数就是 include 自己的那个类,然后就随便折腾了,这种手法在 N 多项目中都有用到,随便找个复杂点的 gem 看看就明白的

文采牛 b 啊~能够把枯燥的东西写的有趣的人一定很有趣

#1 楼 @yzhrain method_missing 不定义方法,define_method 才是。

self.class.send :define_method, method_id do # method body end

这是定义到 class 上的

翻译的的确太好了~~ 赞一个!

不过话说回来, method_missing 和 define_method 也没那么容易混淆, 可以用两个字来分别归纳:

前者叫做 拦截 , 对所有未知的方法进行拦截. 这种方法, rails 里面比比皆是. 后者类似于委托 , Ruby 标准库中, 使用这种办法, 将类实力方法自动委托为类方法, 示例也是比比皆是.

#4 楼 @dotnil

我发现帖子原作者, 貌似使用 send 有点泛滥啊, 当然这是一种风格, 不过确实有点太过了

不过我很想求证一点: 使用 instance_eval 和 send 两种方式, 有没有性能上的差别.

(就像 eval 比 send 要慢一些那样)

#7 楼 @skandhas

那是不是就意味着, 应该多使用 send, 而不是 instance_eval 或 class_eval 呢?

#8 楼 @zw963 send 在 RubyVM 中有优化。

很多 instance_eval 或 class_eval 的使用场景 是用 send 无法替代的。 另外,性能瓶颈一般也不会出现在这个地方,所以我觉得也不用太在意 :)

#9 楼 @skandhas

恩. 我只是就以上示例内具体使用场景来说的, 在以上情况下, 如果 define_method 自身有一些性能限制的话 , 的确不用在意. 但是如果 define_method 自身没有任何影响的话, 使用 send 和 class_eval 还是有些区别的吧 ?

#9 楼 @skandhas

我还是觉的 send 有优化, 也只是在 某个特殊调用方式 的前提下, 而那可能正好是 send 最擅长工作的方式. 而在以上场景中 (适用于 class_eval 的场景), 无论是使用 class_eval 还是 send, 他们的效率是完全一样的. (或者说没有优化. 跟 define_method 应该没有任何关系).

只有这样, 我觉得才可以讲得通, class_eval 这样存在的也更加有意义

而且也只有这种情况下, 也才能说服我, 总是优先使用 class_eval.

(以上纯属站在语言的角度臆测, 没有任何依据)

#11 楼 @zw963 从 Ruby 层面看,我们完全可以忽略 send 是否做了优化,那毕竟是语言实现层面的事,与 Ruby 语言本身没关系。*eval{block} 与 send 的性能相差很少,而且,如果换到别的 Ruby 实现下,有可能情况又不一样了。

所以不用太纠结了,还是以代码的易读为主。

#11 楼 @zw963

在上面的场景里——构建 find_by_xxx 方法——还是用 send 更好。 对于要动态构建方法名的场景,用 class_eval 的话,写的代码就是一堆 string 了,可读性不佳。

内容是精彩的,排版是相当挫的

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