Ruby元编程比听起来还酷

本文概述

  • 元编程
  • 基础
  • 元类
  • 使用” class_eval” 和” instance_eval” 定义方法
  • 快速定义丢失的方法
  • 本文总结
你经常会听到元编程仅是Ruby忍者使用的东西, 而根本不适合普通人使用。但事实是, 元编程一点也不可怕。这篇博客文章将挑战这种思维方式, 并使元编程更接近普通的Ruby开发人员, 以便他们也可以从中受益。
Ruby元编程:代码编写代码
鸣叫
应该注意的是, 元编程可能意味着很多, 它经常会被非常误用, 并且在使用时会变得极端。因此, 我将尝试列举一些现实世界中的示例, 供大家在日常编程中使用。
元编程 元编程是一种技术, 通过该技术, 你可以编写可在运行时自行动态编写代码的代码。这意味着你可以在运行时定义方法和类。疯狂吧?简而言之, 使用元编程, 你可以重新打开和修改类, 捕获不存在的方法并即时创建它们, 通过避免重复创建DRY代码等等。
基础 在深入进行认真的元编程之前, 我们必须探索基础知识。最好的方法是通过示例。让我们从一个开始, 逐步了解Ruby元编程。你可能会猜出这段代码在做什么:
class Developerdef self.backend "I am backend developer" enddef frontend "I am frontend developer" endend

我们用两个方法定义了一个类。此类中的第一个方法是类方法, 第二个是实例方法。这是Ruby中的基本知识, 但是在继续进行之前, 此代码背后还有很多事情需要我们理解。值得指出的是, 类Developer实际上是一个对象。在Ruby中, 一切都是对象, 包括类。由于Developer是实例, 因此它是Class类的实例。 Ruby对象模型如下所示:
Ruby元编程比听起来还酷

文章图片
p Developer.class # Class p Class.superclass # Module p Module.superclass # Object p Object.superclass # BasicObject

这里要了解的一件事是自我的含义。前端方法是在Developer类的实例上可用的常规方法, 但是为什么后端方法是类方法?在Ruby中执行的每段代码都是针对特定的self执行的。当Ruby解释器执行任何代码时, 它始终会跟踪任何给定行的值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 endp Developer.backend # Developer

很好, 但是类方法到底是什么?在回答这个问题之前, 我们需要提到一个称为元类的东西的存在, 也称为单例类和本征类。我们之前定义的类方法前端不过是在元类中为对象Developer定义的实例方法!元类本质上是Ruby创建的一个类, 并插入到继承层次结构中以容纳类方法, 因此不会干扰从该类创建的实例。
元类 Ruby中的每个对象都有其自己的元类。它在某种程度上对于开发人员是不可见的, 但是它在那里并且你可以非常容易地使用它。由于我们的开发人员类本质上是一个对象, 因此它具有自己的元类。举例来说, 我们创建一个String类的对象并操作其元类:
example = "I'm a string object"def example.something self.upcase endp example.something # I'M A STRING OBJECT

我们在这里所做的是向对象添加了单例方法。类方法和单例方法之间的区别在于, 类方法可用于类对象的所有实例, 而单例方法仅可用于该单个实例。类方法被广泛使用, 而单例方法则没有那么多, 但是两种类型的方法都添加到了该对象的元类中。
前面的示例可以这样重写:
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定义后端类方法。我们没有使用self, 而是通过定义这样的方法有效地使其成为类方法。
class Developer class < < self def backend "I am backend developer" end end end

同样, 我们正在定义一个类方法, 但是使用类似于我们用来为String对象定义单例方法的语法。你可能会注意到, 我们在此处使用self指的是Developer对象本身。首先, 我们打开Developer类, 使自己等于Developer类。接下来, 我们对class < < self进行分类, 使self等于开发人员的元类。然后, 我们在开发人员的元类上定义方法后端。
class < < Developer def backend "I am backend developer" end end

通过定义这样的块, 我们可以在块的持续时间内将self设置为开发人员的元类。结果, 后端方法被添加到Developer的元类中, 而不是类本身。
让我们看看该元类在继承树中的行为:
Ruby元编程比听起来还酷

文章图片
正如你在前面的示例中看到的那样, 没有真正的证据证明元类甚至存在。但是我们可以使用一些技巧来向我们展示这个不可见类的存在:
class Object def metaclass_example class < < self self end end end

如果我们在Object类中定义了一个实例方法(是的, 我们可以随时重新打开任何类, 这是元编程的另一优点), 我们将拥有一个内部引用Object对象的自我。然后, 我们可以使用类< < self语法来更改当前self, 以指向当前对象的元类。由于当前对象是Object类本身, 因此它将是实例的元类。该方法返回self, 此时self是一个元类。因此, 通过在任何对象上调用此实例方法, 我们可以获得该对象的元类。让我们再次定义我们的Developer类, 然后开始进行一些探索:
class Developerdef frontend p "inside instance method, self is: " + self.to_s endclass < < self def backend p "inside class method, self is: " + self.to_s end endenddeveloper = 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> > "

对于渐进式, 让我们来看一下证明:前端是类的实例方法, 而后端是元类的实例方法:
p developer.class.instance_methods false # [:frontend]p developer.class.metaclass_example.instance_methods false # [:backend]

不过, 要获取元类, 你无需实际重新打开Object并添加此技巧。你可以使用Ruby提供的singleton_class。它与我们添加的metaclass_example相同, 但是通过此hack, 你实际上可以了解Ruby的工作原理:
p developer.class.singleton_class.instance_methods false # [:backend]

使用” class_eval” 和” instance_eval” 定义方法 还有一种创建类方法的方法, 即使用instance_eval:
class Developer endDeveloper.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_method与method_missing结合使用以编写DRY代码外, 没有什么区别。确切地说, 在定义类时, 可以使用define_method而不是def来操纵范围, 但这完全是另外一回事了。让我们看一个简单的例子:
class Developer define_method :frontend do |*my_arg| my_arg.inject(1, :*) endclass < < self def create_backend singleton_class.send(:define_method, "backend") do "Born from the ashes!" end end end enddeveloper = Developer.new p developer.frontend(2, 5, 10) # => 100p Developer.backend # undefined method 'backend' for Developer:Class (NoMethodError)Developer.create_backend p Developer.backend # "Born from the ashes!"

这显示了如何在不使用def的情况下使用define_method创建实例方法。但是, 我们可以为他们做更多的事情。让我们看一下以下代码片段:
class Developerdef coding_frontend p "writing frontend" enddef coding_backend p "writing backend" endenddeveloper = Developer.newdeveloper.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 endenddeveloper = Developer.newdeveloper.coding_frontend # "writing frontend"developer.coding_backend # "writing backend"

更好, 但仍不完美。为什么?例如, 如果要添加新的方法encoding_debug, 则需要将此” debug” 放入数组中。但是使用method_missing我们可以解决此问题:
class Developerdef 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 endenddeveloper = Developer.newdeveloper.coding_frontend developer.coding_backend developer.coding_debug

这段代码有点复杂, 所以让我们分解一下。调用不存在的方法将触发method_missing。在这里, 我们只想在方法名称以” coding_” 开头时创建一个新方法。否则, 我们仅调用super来完成报告实际上缺少的方法的工作。而且我们只是使用define_method创建该新方法。而已!通过这段代码, 我们可以创建从” coding_” 开始的数以千计的新方法, 而这就是使我们的代码变干的原因。由于define_method恰好是Module私有的, 因此我们需要使用send来调用它。
本文总结 这只是冰山一角。要成为Ruby Jedi, 这是起点。掌握了元编程的这些构成要素并真正理解了其本质之后, 你可以进行更复杂的操作, 例如创建自己的领域特定语言(DSL)。 DSL本身就是一个主题, 但是这些基本概念是理解高级主题的前提。 Rails中一些最常用的gem是用这种方式构建的, 你甚至可能在不知道它的情况下使用了它的DSL, 例如RSpec和ActiveRecord。
【Ruby元编程比听起来还酷】希望本文可以使你更进一步地了解元编程, 甚至可以构建自己的DSL, 从而可以更有效地进行编码。

    推荐阅读