当undef_method遇上method_missing

写在前面

使用undef_method, 遇到的一个问题,想不明白rails是如何处理的。最后在rubyChina上发帖求助,没成想得到了很耐心的解答,贴上,以期可飨同惑者。

正文

先看一段调用undef_method的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module MyModule
def my_value
"my_value in MyModule"
end
end

class MyClass
attr_accessor :my_value

def initialize(value)
@my_value = value
end
end

class MySubClass < MyClass
include MyModule
undef_method :my_value
end

object = MySubClass.new(20)
puts object.my_value

执行,很明显,会报错,抛出NoMethodError

1
undefined method `my_value' for #<MySubClass:0x007fbdc4065818 @my_value=20>

没毛病,正常,ruby 2.5中对undef_method 的定义就是这样的,prevents the current class from responding to calls to the named methods。

But,你会发现rails中有一个神奇的地方,以一个简单的user model为例:

user model含有字段name, 同时include了一个module UserAddon,使用undef_method来禁止调用引入的name方法,按照undef_method的定义, 此时User的对象调用name时,应该会报错,但是进入终端后,发现没有,反而返回了正确的结果。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
## app/models/user.rb

# == Schema Information
#
# Table name: users
#
# id :integer not null, primary key
# email :string(254)
# name :string(254)
# password_digest :string(254)
# created_at :datetime not null
# updated_at :datetime not null

class User < ActiveRecord
include UserAddon
undef_method :name
end

## app/models/users_addon.rb
module UserAddon
def name
"hello"
end
end

这时,rails c 进入console:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
user = User.first

# => #<User id: 1, name: "admin", email: "test@gmail.com", created_at:……>

user.methods.include?(:name)
# => false

user.name

# => "admin"

$ user.name

# => Couldn't locate a definition for user.name

user.respond_to?(:name)
# => true

使用pry的$ ,找不到user.name的定义,但是执行user.name却返回了正确的结果,此时一定调用了method_missing。但是,如何查看rails是如何实现的呢?

ruby元编程中在有关 rails的属性方法中曾经提到,当第一次访问一个属性时,这个属性是一个幽灵方法,ActiveRecord::Base # method_missing() 会把它转换成一个真实的方法,同时创建出读,写,查询方法,比如上面的name,会创建name, name=, name?,这样下次就可以直接调用。

但是,这里很显然,user.methods.include?(:name)返回了false,这里并没有创建name这个方法,那么rails是如何让user.name 返回了”admin”的呢?

这里附上来自IChou 大神的解答:

使用undef_method :name后,调用user.name, 它会落入下面的 method_missing 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# lib/active_model/attribute_methods.rb:425

def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true) # => 这里是 false
super
else
match = matched_attribute_method(method.to_s)
#<struct ActiveModel::AttributeMethods::ClassMethods::AttributeMethodMatcher::AttributeMethodMatch target="attribute", attr_name="name", method_name="name">
match ? attribute_missing(match, *args, &block) : super
end
end

def attribute_missing(match, *args, &block)
__send__(match.target, match.attr_name, *args, &block)
# match.target => "attribute", 这里调用了自身的 attribute 方法
end

# lib/active_record/attribute_methods/read.rb:76
def _read_attribute(attr_name) # :nodoc:
@attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? }
end
alias :attribute :_read_attribute
private :attribute

「也就是最后调用@attributes.fetch_value(attr_name.to_s) ,user.name的返回了正确的结果,而不是如预期所想的那样抛出NoMethodError。」

参考

rails attribute methods

undef method