Ruby 元编程比听起来更酷

已发表: 2022-03-11

您经常听到元编程是只有 Ruby 忍者使用的东西,而且它根本不适合普通人。 但事实是,元编程一点也不可怕。 这篇博文将挑战这种思维方式,让元编程更接近普通的 Ruby 开发人员,以便他们也能从中受益。

Ruby 元编程:代码编写代码
鸣叫

应该注意的是,元编程可能意味着很多,而且它经常被滥用并且在使用时会走向极端,所以我将尝试提供一些每个人都可以在日常编程中使用的现实世界的例子。

元编程

元编程是一种技术,您可以通过它编写代码,在运行时自行动态编写代码。 这意味着您可以在运行时定义方法和类。 疯了吧? 简而言之,使用元编程,您可以重新打开和修改类、捕获不存在的方法并即时创建它们、通过避免重复创建 DRY 代码等等。

基础

在我们深入研究元编程之前,我们必须探索基础知识。 做到这一点的最好方法就是举例。 让我们从一个开始,逐步了解 Ruby 元编程。 您可能会猜到这段代码在做什么:

 class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" end end

我们定义了一个有两个方法的类。 这个类中的第一个方法是类方法,第二个是实例方法。 这是 Ruby 中的基本内容,但在我们继续进行之前,我们需要了解这些代码背后发生的更多事情。 值得指出的是, Developer类本身实际上是一个对象。 在 Ruby 中,一切都是对象,包括类。 由于Developer是一个实例,它是类Class的一个实例。 下面是 Ruby 对象模型的样子:

Ruby 对象模型

 p Developer.class # Class p Class.superclass # Module p Module.superclass # Object p Object.superclass # BasicObject

这里要理解的一件重要事情是self的含义。 frontend方法是在类Developer的实例上可用的常规方法,但为什么backend方法是类方法? 在 Ruby 中执行的每一段代码都是针对特定的self执行的。 当 Ruby 解释器执行任何代码时,它总是跟踪任何给定行的值selfself总是引用某个对象,但该对象可以根据执行的代码而改变。 例如,在类定义中, self指的是类本身,它是类Class的一个实例。

 class Developer p self end # Developer

在实例方法中, self指的是类的一个实例。

 class Developer def frontend self end end p Developer.new.frontend # #<Developer:0x2c8a148>

在类方法中, self以某种方式引用类本身(本文稍后将详细讨论):

 class Developer def self.backend self end end p Developer.backend # Developer

这很好,但到底什么是类方法? 在回答这个问题之前,我们需要提到元类的存在,也称为单例类和特征类。 我们之前定义的类方法frontend只不过是在元类中为对象Developer定义的实例方法! 元类本质上是 Ruby 创建并插入到继承层次结构中以保存类方法的类,因此不会干扰从该类创建的实例。

元类

Ruby 中的每个对象都有自己的元类。 它对开发人员来说是不可见的,但它就在那里,你可以很容易地使用它。 由于我们的类Developer本质上是一个对象,因此它有自己的元类。 作为一个例子,让我们创建一个String类的对象并操作它的元类:

 example = "I'm a string object" def example.something self.upcase end p example.something # I'M A STRING OBJECT

我们在这里所做的something向对象添加了一个单例方法。 类方法和单例方法之间的区别在于,类方法可用于类对象的所有实例,而单例方法仅可用于该单个实例。 类方法被广泛使用,而单例方法没有那么多,但两种类型的方法都被添加到该对象的元类中。

前面的例子可以这样重写:

 example = "I'm a string object" class << example def something self.upcase end end

语法不同,但它有效地做同样的事情。 现在让我们回到之前创建Developer类的示例,并探索一些其他语法来定义类方法:

 class Developer def self.backend "I am backend developer" end end

这是几乎每个人都使用的基本定义。

 def Developer.backend "I am backend developer" end

这是同一件事,我们正在为Developer定义backend类方法。 我们没有使用self但定义这样的方法有效地使其成为类方法。

 class Developer class << self def backend "I am backend developer" end end end

同样,我们正在定义一个类方法,但使用的语法类似于我们为String对象定义单例方法的语法。 您可能会注意到我们在这里使用了self ,它指的是Developer对象本身。 首先我们打开Developer类,使self等于Developer类。 接下来,我们做class << self ,使 self 等于Developer的元类。 然后我们在Developer的元类上定义一个方法backend

 class << Developer def backend "I am backend developer" end end

通过定义这样的块,我们在块的持续时间内将self设置为Developer的元类。 结果, backend方法被添加到Developer的元类中,而不是类本身。

让我们看看这个元类在继承树中的表现:

继承树中的元类

正如您在前面的示例中看到的,没有真正的证据表明元类甚至存在。 但是我们可以使用一个小技巧来向我们展示这个隐形类的存在:

 class Object def metaclass_example class << self self end end end

如果我们在Object类中定义一个实例方法(是的,我们可以随时重新打开任何类,这是元编程的另一个优点),我们将有一个self引用其中的Object对象。 然后我们可以使用class << self语法将当前self更改为指向当前对象的元类。 由于当前对象是Object类本身,这将是实例的元类。 该方法返回self ,此时它本身就是一个元类。 因此,通过在任何对象上调用此实例方法,我们可以获得该对象的元类。 让我们再次定义我们的Developer类并开始探索:

 class Developer def frontend p "inside instance method, self is: " + self.to_s end class << self def backend p "inside class method, self is: " + self.to_s end end end developer = Developer.new developer.frontend # "inside instance method, self is: #<Developer:0x2ced3b8>" Developer.backend # "inside class method, self is: Developer" p "inside metaclass, self is: " + developer.metaclass_example.to_s # "inside metaclass, self is: #<Class:#<Developer:0x2ced3b8>>"

对于渐强,让我们看看frontend是类的实例方法而backend是元类的实例方法的证明:

 p developer.class.instance_methods false # [:frontend] p developer.class.metaclass_example.instance_methods false # [:backend]

虽然,要获得元类,您实际上不需要重新打开Object并添加此 hack。 您可以使用 Ruby 提供的singleton_class 。 它与我们添加的metaclass_example相同,但是通过这个 hack,您实际上可以看到 Ruby 是如何在幕后工作的:

 p developer.class.singleton_class.instance_methods false # [:backend]

使用“class_eval”和“instance_eval”定义方法

还有另一种创建类方法的方法,那就是使用instance_eval

 class Developer end Developer.instance_eval do p "instance_eval - self is: " + self.to_s def backend p "inside a method self is: " + self.to_s end end # "instance_eval - self is: Developer" Developer.backend # "inside a method self is: Developer"

这段代码 Ruby 解释器在一个实例的上下文中进行评估,在本例中是一个Developer对象。 当您在对象上定义方法时,您正在创建类方法或单例方法。 在这种情况下,它是一个类方法——确切地说,类方法是单例方法,但是是类的单例方法,而其他的是对象的单例方法。

另一方面, class_eval在类而不是实例的上下文中评估代码。 它实际上重新打开了课程。 下面是如何使用class_eval创建实例方法:

 Developer.class_eval do p "class_eval - self is: " + self.to_s def frontend p "inside a method self is: " + self.to_s end end # "class_eval - self is: Developer" p developer = Developer.new # #<Developer:0x2c5d640> developer.frontend # "inside a method self is: #<Developer:0x2c5d640>"

总而言之,当您调用class_eval方法时,您将self更改为引用原始类,而当您调用instance_eval时, self更改为引用原始类的元类。

动态定义缺失的方法

还有一个元编程难题是method_missing 。 当您调用对象的方法时,Ruby 首先进入类并浏览其实例方法。 如果它在那里没有找到该方法,它会继续向上搜索祖先链。 如果 Ruby 仍然没有找到该方法,它会调用另一个名为method_missing的方法,这是每个对象都继承的Kernel的实例方法。 由于我们确信 Ruby 最终会为缺少的方法调用此方法,因此我们可以使用它来实现一些技巧。

define_method是在Module类中定义的方法,您可以使用它来动态创建方法。 要使用define_method ,您可以使用新方法的名称和一个块来调用它,其中块的参数成为新方法的参数。 使用def创建方法和define_method有什么区别? 除了您可以结合使用define_methodmethod_missing来编写 DRY 代码之外,没有太大区别。 确切地说,您可以在定义类时使用define_method而不是def来操作范围,但这是另一回事。 我们来看一个简单的例子:

 class Developer define_method :frontend do |*my_arg| my_arg.inject(1, :*) end class << self def create_backend singleton_class.send(:define_method, "backend") do "Born from the ashes!" end end end end developer = Developer.new p developer.frontend(2, 5, 10) # => 100 p Developer.backend # undefined method 'backend' for Developer:Class (NoMethodError) Developer.create_backend p Developer.backend # "Born from the ashes!"

这显示了如何使用define_method在不使用def的情况下创建实例方法。 然而,我们可以用它们做更多的事情。 让我们看一下这段代码片段:

 class Developer def coding_frontend p "writing frontend" end def coding_backend p "writing backend" end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"

这段代码不是 DRY,但使用define_method我们可以使它成为 DRY:

 class Developer ["frontend", "backend"].each do |method| define_method "coding_#{method}" do p "writing " + method.to_s end end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"

这好多了,但仍然不完美。 为什么? 例如,如果我们想添加一个新方法coding_debug ,我们需要将这个"debug"放入数组中。 但是使用method_missing我们可以解决这个问题:

 class Developer def method_missing method, *args, &block return super method, *args, &block unless method.to_s =~ /^coding_\w+/ self.class.send(:define_method, method) do p "writing " + method.to_s.gsub(/^coding_/, '').to_s end self.send method, *args, &block end end developer = Developer.new developer.coding_frontend developer.coding_backend developer.coding_debug

这段代码有点复杂,让我们分解一下。 调用不存在的方法将启动method_missing 。 在这里,我们只想在方法名称以"coding_"开头时创建一个新方法。 否则,我们只是调用 super 来完成报告实际缺失的方法的工作。 我们只是使用define_method来创建新方法。 而已! 使用这段代码,我们可以创建数以千计的以"coding_"开头的新方法,而这正是我们的代码 DRY 的原因。 由于define_method恰好是Module私有的,我们需要使用send来调用它。

包起来

这只是冰山一角。 成为一名红宝石绝地,这是起点。 在你掌握了元编程的这些构建块并真正理解了它的本质之后,你可以继续做一些更复杂的事情,例如创建你自己的领域特定语言 (DSL)。 DSL 本身就是一个主题,但这些基本概念是理解高级主题的先决条件。 Rails 中一些最常用的 gem 就是以这种方式构建的,您可能在不知道的情况下使用了它的 DSL,例如 RSpec 和 ActiveRecord。

希望这篇文章能让你更接近理解元编程,甚至可能构建你自己的 DSL,你可以用它来更有效地编码。