使用 define_method 和元编程在 Ruby 中动态定义实例方法?
Using define_method and meta-programming to define instance methods dynamically in Ruby?
我有一些类似的代码:
class Country
attr_reader :name
def initialize
@name = "MyName".freeze
end
def government
@government ||= Government.new(self)
end
def symbols
@symbols ||= Symbols.new(self)
end
def economy
@economy ||= Economy.new(self)
end
def education
@education ||= Education.new(self)
end
def healthcare
@healthcare ||= Healthcare.new(self)
end
def holidays
@holidays ||= Holidays.new(self)
end
def religion
@religion ||= Religion.new(self)
end
end
如何动态创建方法?我试过了:
class Country
attr_reader :name
COMPONENETS = %w(government symbols economy education healthcare holidays religion)
COMPONENETS.each do |m|
define_method(m) do |argument|
instance_variable_set("@#{m}",Object.const_get(m.capitalize).new(self))
end
end
def initialize
@name = "MyName".freeze
end
end
如果我尝试:
puts Country.new.education.inspect
我收到以下错误:
country.rb:16:in `block (2 levels) in <class:Country>': wrong number of arguments (0 for 1) (ArgumentError)
from country.rb:27:in `<main>'
我在这里错过了什么?
我猜你不需要 argument
:
class Country
attr_reader :name
COMPONENETS = %w(government symbols economy education healthcare holidays religion)
COMPONENETS.each do |m|
define_method(m) do
instance_variable_set("@#{m}",Object.const_get(m.capitalize).new(self))
end
end
def initialize
@name = "MyName".freeze
end
end
您可以简单地使用 eval :
class Country
attr_reader :name
COMPONENETS = %w(government symbols economy education healthcare holidays religion)
COMPONENETS.each do |m|
eval <<-DEFINE_METHOD
def #{m}
@#{m} ||= #{m.capitalize}.new(self)
end
DEFINE_METHOD
end
def initialize
@name = "MyName".freeze
end
end
在您的原始代码中,您将所有方法定义为不带参数:
def education
# ^^^
@education ||= Education.new(self)
end
在元编程代码中,您将所有方法定义为采用名为 argument
:
的单个参数
define_method(m) do |argument|
# ^^^^^^^^^^
instance_variable_set("@#{m}", Object.const_get(m.capitalize).new(self))
end
但是,您使用零参数调用它:
puts Country.new.education.inspect
# ^^^
显然,您的方法是惰性 getter,因此它们不应该带任何参数:
define_method(m) do
instance_variable_set("@#{m}", Object.const_get(m.capitalize).new(self))
end
请注意,您的代码还有其他问题。在您的原始代码中,您使用条件赋值仅在实例变量未定义 nil
或 false
时执行赋值,而在元编程代码中,您始终无条件地设置它。它应该更像这样:
define_method(m) do
if instance_variable_defined?(:"@#{m}")
instance_variable_get(:"@#{m}")
else
instance_variable_set(:"@#{m}", const_get(m.capitalize).new(self))
end
end
注意:我还从对 const_get
的调用中删除了 Object.
以使用正常的常量查找规则查找常量(即首先在词法上向外然后在继承层次结构中向上),因为这对应于您在原始代码片段中查找常量的方式。
这并不完全等同于您的代码,因为它仅在实例变量未定义时设置实例变量,而不是在 false
或 nil
时设置实例变量,但我想这更接近于无论如何你的意图。
我将封装此代码以使其意图更清楚:
class Module
def lazy_attr_reader(name, default=(no_default = true), &block)
define_method(name) do
if instance_variable_defined?(:"@#{name}")
instance_variable_get(:"@#{name}")
else
instance_variable_set(:"@#{name}",
if no_default then block.(name) else default end)
end
end
end
end
class Country
attr_reader :name
COMPONENTS = %w(government symbols economy education healthcare holidays religion)
COMPONENTS.each do |m|
lazy_attr_reader(m) do |name|
const_get(name.capitalize).new(self))
end
end
def initialize
@name = 'MyName'.freeze
end
end
这样,阅读你的 Country
class 的人就不会去 "Huh, so there is this loop which defines methods which sometimes get and sometimes set instance variables",而是认为 "Ah, this is a loop which creates lazy getters!"
我有一些类似的代码:
class Country
attr_reader :name
def initialize
@name = "MyName".freeze
end
def government
@government ||= Government.new(self)
end
def symbols
@symbols ||= Symbols.new(self)
end
def economy
@economy ||= Economy.new(self)
end
def education
@education ||= Education.new(self)
end
def healthcare
@healthcare ||= Healthcare.new(self)
end
def holidays
@holidays ||= Holidays.new(self)
end
def religion
@religion ||= Religion.new(self)
end
end
如何动态创建方法?我试过了:
class Country
attr_reader :name
COMPONENETS = %w(government symbols economy education healthcare holidays religion)
COMPONENETS.each do |m|
define_method(m) do |argument|
instance_variable_set("@#{m}",Object.const_get(m.capitalize).new(self))
end
end
def initialize
@name = "MyName".freeze
end
end
如果我尝试:
puts Country.new.education.inspect
我收到以下错误:
country.rb:16:in `block (2 levels) in <class:Country>': wrong number of arguments (0 for 1) (ArgumentError)
from country.rb:27:in `<main>'
我在这里错过了什么?
我猜你不需要 argument
:
class Country
attr_reader :name
COMPONENETS = %w(government symbols economy education healthcare holidays religion)
COMPONENETS.each do |m|
define_method(m) do
instance_variable_set("@#{m}",Object.const_get(m.capitalize).new(self))
end
end
def initialize
@name = "MyName".freeze
end
end
您可以简单地使用 eval :
class Country
attr_reader :name
COMPONENETS = %w(government symbols economy education healthcare holidays religion)
COMPONENETS.each do |m|
eval <<-DEFINE_METHOD
def #{m}
@#{m} ||= #{m.capitalize}.new(self)
end
DEFINE_METHOD
end
def initialize
@name = "MyName".freeze
end
end
在您的原始代码中,您将所有方法定义为不带参数:
def education
# ^^^
@education ||= Education.new(self)
end
在元编程代码中,您将所有方法定义为采用名为 argument
:
define_method(m) do |argument|
# ^^^^^^^^^^
instance_variable_set("@#{m}", Object.const_get(m.capitalize).new(self))
end
但是,您使用零参数调用它:
puts Country.new.education.inspect
# ^^^
显然,您的方法是惰性 getter,因此它们不应该带任何参数:
define_method(m) do
instance_variable_set("@#{m}", Object.const_get(m.capitalize).new(self))
end
请注意,您的代码还有其他问题。在您的原始代码中,您使用条件赋值仅在实例变量未定义 nil
或 false
时执行赋值,而在元编程代码中,您始终无条件地设置它。它应该更像这样:
define_method(m) do
if instance_variable_defined?(:"@#{m}")
instance_variable_get(:"@#{m}")
else
instance_variable_set(:"@#{m}", const_get(m.capitalize).new(self))
end
end
注意:我还从对 const_get
的调用中删除了 Object.
以使用正常的常量查找规则查找常量(即首先在词法上向外然后在继承层次结构中向上),因为这对应于您在原始代码片段中查找常量的方式。
这并不完全等同于您的代码,因为它仅在实例变量未定义时设置实例变量,而不是在 false
或 nil
时设置实例变量,但我想这更接近于无论如何你的意图。
我将封装此代码以使其意图更清楚:
class Module
def lazy_attr_reader(name, default=(no_default = true), &block)
define_method(name) do
if instance_variable_defined?(:"@#{name}")
instance_variable_get(:"@#{name}")
else
instance_variable_set(:"@#{name}",
if no_default then block.(name) else default end)
end
end
end
end
class Country
attr_reader :name
COMPONENTS = %w(government symbols economy education healthcare holidays religion)
COMPONENTS.each do |m|
lazy_attr_reader(m) do |name|
const_get(name.capitalize).new(self))
end
end
def initialize
@name = 'MyName'.freeze
end
end
这样,阅读你的 Country
class 的人就不会去 "Huh, so there is this loop which defines methods which sometimes get and sometimes set instance variables",而是认为 "Ah, this is a loop which creates lazy getters!"