【问题标题】:Ruby metaprogramming: How can make module method see class's variableRuby 元编程:如何让模块方法看到类的变量
【发布时间】:2017-06-21 17:22:05
【问题描述】:

例如我有一个模块和一个类:

module SimpleModule
  def self.class_hello
    puts "hello from #{@@name}"
  end
end

class SimpleClass
  @@name = 'StackOverFlow'
  def self.test
    SimpleModule.class_hello
  end
end

然后我通过从类中调用模块方法进行测试:

SimpleClass.test

我遇到了异常:

SimpleModule 中未初始化的类变量@@name (NameError)

我知道这里是因为模块的范围与类的范围不同。所以我的问题是:如何为 SimpleModule 范围共享 SimpleClass 范围?

我放元编程是因为这里只是一个简单的例子,之后我将通过从动态类调用动态模块来推进。 (这就是我不想使用诸如 includeextend 之类的关键字的原因)

@编辑 事实上,我想自己实现 Ruby 扩展。这是我已经开发的版本:

# implementation
class Class
  def custom_extend(module_name)
    module_name.methods(false).each do |method|
      define_singleton_method(method) do |*args, &block|
        module_name.send(method, *args, &block)
      end
    end
  end
end

这是我用于测试的自定义模块和类:

# -------------------------------------------------------------
# Demonstration
module SimpleModule
  def self.class_hello_world
    puts 'i am a simple module boss'
  end

  def self.class_hello_name
    puts "hello from #{@@name}"
  end
end

class SimpleClass
  custom_extend  SimpleModule
  @@name = 'StackOverFlow'
end

这是我的两个测试:

SimpleClass.class_hello_world  # work
SimpleClass.class_hello_name   # not work

【问题讨论】:

  • 为什么不将类添加到模块中?
  • @user000001 正如我所说,这是一个简化的代码。真正的事情是我再次实现了 ruby​​-liked includeextend 关键字。因此,我需要以编程方式将所有函数从模块导入到类。我现在可以做到这一点。我现在唯一不能做的是在两者之间共享范围。

标签: ruby metaprogramming


【解决方案1】:

更新答案

这是您的代码稍作修改的版本。不需要includeextendappend_featuresmodule_function。使用相同的结构添加custom_include 并不难。

更新:请务必阅读@7stud 的answer,其结构相似且解释非常好。

class Class
  def custom_extend(module_name)
    module_name.instance_methods(false).each do |method|
      define_singleton_method(method) do |*args, &block|
        module_name.instance_method(method).bind(self).call(*args, &block)
      end
    end
  end
end

module SimpleModule
  def class_hello
    puts "hello from #{@name}"
  end
end

class SimpleClass
  @name = 'class'
  custom_extend SimpleModule

  def self.test
    class_hello
  end
end

SimpleClass.test
#=> hello from class

原答案

常规方式

通常的方法是:

module SimpleModule
  def class_hello
    puts "hello from #{@name}"
  end
end

class SimpleClass
  @name = 'StackOverFlow'
  extend SimpleModule

  def self.test
    class_hello
  end
end

SimpleClass.class_hello

但你不想要它。 (为什么?)

你的方式

在您的代码中,SimpleClassSimpleModule 完全相互独立。很明显,您得到了NameError。您需要以某种方式传递name 信息。

作为名称参数:

module SimpleModule
  def self.class_hello(name='')
    puts "hello from #{name}"
  end
end

class SimpleClass
  @@name = 'StackOverFlow'
  def self.test
    SimpleModule.class_hello(@@name)
  end
end

带klass参数:

module SimpleModule
  def self.class_hello(calling_class=self)
    calling_class.class_eval{
      puts "hello from #{@name}"
    }
  end
end

class SimpleClass
  @name = 'StackOverFlow'
  def self.test
    SimpleModule.class_hello(self)
  end
end

SimpleClass.test

binding参数:

module SimpleModule
  def self.class_hello(b)
    puts "hello from #{b.eval('@@name')}"
  end
end

class SimpleClass
  @@name = 'StackOverFlow'
  def self.test
    SimpleModule.class_hello(binding)
  end
end

SimpleClass.test

my_ruby_extend SimpleModule

这当然可以通过自定义my_ruby_extend 来完成。不过,您需要显示所需的语法,以及您已经实现的内容。

这样,您可以告诉 Ruby SimpleClassSimpleModule 是链接的。当在SimpleModule 中找不到方法或变量时,可以在SimpleClass 中查找。

【讨论】:

  • 感谢您的回答。我的目的是尝试重新实现 extend 关键字,这就是我不想使用的原因:1. extends 关键字(很明显)。 2. 给模块添加参数。 (因为模块的实现不需要知道谁应该真正调用它)。
  • 谢谢。我只是尝试重新实现 extend 作为练习。我想测试是否可以通过使用其核心语言(例如 attr_accessor、alias ...)来完成 Ruby 中的任何事情,是否可以通过使用 Ruby 元编程来实现。所以也许如你所说,Ruby 中的 extendinclude 是 Ruby 核心语言的一部分,必须在 C++ 层完成。
  • C,而不是C++ ;)。我想说你可以用普通的 Ruby 实现my_extend。您需要定义它并以某种方式使用它,以便告诉 Ruby SimpleClass 已使用 SimpleModule 进行“my_extended”。您的问题中缺少此信息。顺便说一句,听起来鲁比尼乌斯会让你感兴趣。
  • 谢谢。我已经编辑了我的问题,包括我做了什么。请看一下。谢谢。
  • 非常感谢 :D 不错的答案。我可以为你们两个投票 1000 次 ^^^
【解决方案2】:

我只是尝试重新实现扩展作为练习。

ruby 的extend() 不能这样工作:

module SimpleModule
  def self.class_hello_world
    puts 'i am a simple module boss'
  end

  def self.class_hello_name
    puts "hello from #{@@name}"
  end
end

class SimpleClass
  custom_extend  SimpleModule

例如,以下行不通:

module Dog
  def self.greet
    puts "hello"
  end
end

class Cat
  extend Dog
end

Cat.greet

--output:--
`<main>': undefined method `greet' for Cat:Class (NoMethodError)

extend() 是这样工作的:

module Dog
  def greet
    puts "hello"
  end
end

class Cat
  extend Dog
end

Cat.greet

--output:--
hello

换句话说,extend() 将模块实例方法——而不是模块方法(例如,以self 开头的方法名)——插入到 Cat 的单例类(这是 Cat 的类方法所在的位置)。在 ruby​​ 中,include()extend() 与模块方法无关(同样,方法名称以 self 开头)。模块在 ruby​​ 中有两种用途:

  1. 作为命名空间,例如包含def self.method_name
  2. 作为 mixin,例如包含def some_method

include() 和 extend() 处理 #2。

以下解决方案不适用于@@variables,但试图弄清楚@@variables 在ruby 中展示的所有曲折是不值得的——只是不要使用它们。请改用 类实例变量,即在任何 def 之外指定 @variables

def my_extend(some_module)
  singleton_class.include some_module
end

module Dog
  def greet
    puts @greeting
  end

  private
  def sayhi
    puts "hi"
  end
end

class Cat
  @greeting = "hello"
  my_extend Dog
end

Cat.greet
#Cat.sayhi  #=>`<main>': private method `sayhi' called for Cat:Class (NoMethodError) 

Cat.class_eval {sayhi}  #Change self to the Cat class so the implicit
                        #self variable that calls sayhi is equal to Cat

--output:--
hello
hi

现在,您只需实现 my_include 并将其替换为 include。 :)

这是my_include() 的照片:

class Class

  def my_include(module_)
    #For public and protected methods:
    module_.instance_methods(include_super=false).each do |meth_name|
      meth = module_.instance_method(meth_name)
      define_method(meth_name) do 
        meth.bind(self).call
      end
    end

    #For private methods:
    module_.private_instance_methods(include_super=false).each do |meth_name|
      meth = module_.instance_method(meth_name)
      define_method(meth_name) do
        meth.bind(self).call
      end
      private :"#{meth_name}"
    end

  end
end

module Dog
  def greet
    puts "hello"
  end

  def go
    puts "run, run run"
  end

  private
  def sayhi
    puts "hi"
  end

end

class Cat
  my_include Dog
end

c = Cat.new
c.greet
c.go
c.sayhi 

--output:--
hello
run, run run
 #=>`<main>': private method `sayhi' called for #<Cat:0x007fc014136f60> (NoMethodError)

my_extend():

class Class

  def my_include(module_)
    #For public and protected methods:
    module_.instance_methods(include_super=false).each do |meth_name|
      meth = module_.instance_method(meth_name)
      define_method(meth_name) do 
        meth.bind(self).call
      end
    end

    #For private methods:
    module_.private_instance_methods(include_super=false).each do |meth_name|
      meth = module_.instance_method(meth_name)
      define_method(meth_name) do
        meth.bind(self).call
      end
      private :"#{meth_name}"
    end

  end

  def my_extend(module_)
    singleton_class.my_include module_
  end

end


module Dog
  def greet
    puts @greeting
  end

  private
  def sayhi
    puts "hi"
  end
end

class Cat
  @greeting = "hello"
  my_extend Dog
end

Cat.greet
#Cat.sayhi  #=>private method `sayhi' called for Cat:Class (NoMethodError)
Cat.class_eval {sayhi}

--output:--
hello
hi

【讨论】:

  • 很好的代码和很好的解释。我们几乎同时提出了一个非常相似的解决方案。我对module_namemodul 不满意,module_ 是一个很好的折衷方案。我没有考虑私有方法,你的方法不接受参数或块;)
  • @EricDuminil,你的方法不接受参数或块——小孩子的游戏!破解时打字太痛苦了。 我们几乎同时提出了一个非常相似的解决方案——我在一小时前发布了我的解决方案。然后我开始研究私有方法,这有点棘手,我将它们添加到我的示例中。
  • 我并不是说这是一个无法克服的问题。只是可以添加。
  • 哇。太好了 :D 非常感谢你们俩。这里最好的部分是meth.bind(self).call。我以前不知道这一点,所以我很头疼思考如何从类到模块共享范围。我在这里问了一个类似的问题:custom include ruby。这里的人说我们不能自己实现 custom-include :D(因为我不能直接调用模块实例方法)。
  • 注意我又看到了。我不知道为什么原作者删除了一些cmets。但正因为如此,它让我放弃了实现 custom-include 并跳转到 custom-extend :D
【解决方案3】:

简化问题

您可以通过如下修改代码来简单地提出您的问题。

module SimpleModule
  def self.class_hello
    puts "hello from #{@@name}"
  end
end

class SimpleClass
  @@name = 'StackOverFlow'
end

SimpleModule.class_hello
  # NameError: uninitialized class variable @@name in SimpleModule

哪个班?

显然,模块的模块方法class_hello 必须被告知需要其类变量&amp;&amp;name 的类。因此,我们必须将该类作为class_hello 的参数。然后我们使用 [Module#class_variable_get](https://ruby-doc.org/core-2.2.1/Module.html#method-i-class_variable_get ) 来提取类变量的值。

module SimpleModule
  def self.class_hello(klass)
    puts "hello from #{ klass.class_variable_get(:@@name) }"
  end
end

SimpleModule.class_hello(SimpleClass)
  # hello from StackOverFlow

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-23
    • 1970-01-01
    • 2011-02-19
    • 2012-01-06
    • 1970-01-01
    • 2012-04-20
    相关资源
    最近更新 更多