作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
尼古拉·托多罗维奇的头像

尼古拉Todorovic

14 的经验

Nikola拥有MCE学位和近十年的软件开发经验. 他的爱好是Ruby on Rails和创业.

专业知识

分享

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

Ruby元编程:代码编写

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

元编程

元编程 编写代码的技术是这样的吗 代码本身在运行时是动态的. 这意味着您可以在运行时定义方法和类. 疯狂的,对吧? 简而言之, 使用元编程可以重新打开和修改类, 捕捉不存在的方法并动态创建它们, 创建代码 通过避免重复,以及更多.

最基本的

在深入研究元编程之前,我们必须先了解基础知识. 最好的方法就是举例说明. 让我们从一个开始,逐步理解Ruby元编程. 你大概可以猜到这段代码在做什么:

类开发人员

  def自我.后端
    “我是后端开发人员”
  结束
  
  def前端
    “我是前端开发人员”
  结束

结束

我们已经定义了一个带有两个方法的类. 这个类中的第一个方法是类方法,第二个是实例方法. 这是Ruby中的基本内容, 但是在这段代码后面发生了更多的事情,我们需要在继续进行之前了解这些事情. 值得指出的是,班级 开发人员 它本身实际上是一个物体. 在Ruby中,一切都是对象,包括类. 自 开发人员 是一个实例,它是一个类的实例吗 Class. 下面是Ruby对象模型的样子:

Ruby对象模型

p开发人员.类#类
p类.超类# 模块
p模块.超类# Object
p对象.超类# BasicObject

这里要理解的重要一点是 自我. 的 前端 方法是在类的实例上可用的常规方法 开发人员,但为什么呢? 后端 类方法? Ruby中执行的每段代码都是针对特定的 自我. 当Ruby解释器执行任何代码时,它总是跟踪该值 自我 对于任意给定的直线. 自我 总是引用某个对象,但该对象可以根据执行的代码更改. 例如,在类定义中, 自我 指类本身,它是类的一个实例 Class.

类开发人员
  p自我 
结束
#开发商

在实例方法中, 自我 引用类的实例.

类开发人员
  def前端
    自我
  结束
结束
 
p开发人员.新.前端
# #<开发人员:0x2c8a148>

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

类开发人员
  def自我.后端
    自我
  结束
结束

p开发人员.后端
#开发商

这很好,但是什么是类方法呢? 在回答这个问题之前,我们需要提到元类的存在, 也称为单例类和特征类. 类方法 前端 我们前面定义的只是在对象的元类中定义的实例方法 开发人员! 元类本质上是Ruby创建并插入到继承层次结构中以保存类方法的类, 这样就不会干扰从类创建的实例.

元类

Ruby中的每个对象都有自己的对象 元类. 它对开发人员来说是不可见的,但它就在那里,你可以很容易地使用它. 从我们班开始 开发人员 本质上是一个对象,它有自己的元类. 作为一个例子,让我们创建一个类的对象 字符串 并操作它的元类:

example = "I'm a string object"

def的例子.某物
  自我.upcase
结束

p的例子.某物
# i是一个字符串对象

我们在这里添加了一个单例方法 某物 到一个对象. 类方法和单例方法的区别在于类方法对类对象的所有实例可用,而单例方法仅对该实例可用. 类方法被广泛使用,而单例方法则不是那么多, 但是两种类型的方法都被添加到该对象的元类中.

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

example = "I'm a string object"

class << example
  def的东西
    自我.upcase
  结束
结束

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

类开发人员
  def自我.后端
    “我是后端开发人员”
  结束
结束

这是一个基本的定义,几乎每个人都在使用.

def开发人员.后端
  “我是后端开发人员”
结束

这是一样的,我们定义了 后端 的类方法 开发人员. 我们没有使用 自我 但是这样定义一个方法实际上使它成为一个类方法.

类开发人员
  class << 自我
    def的后端
      “我是后端开发人员”
    结束
  结束
结束

再一次。, 我们正在定义一个类方法, 但是使用的语法类似于我们为a定义单例方法时使用的语法 字符串 object. 你可能注意到我们用 自我 这里指的是a 开发人员 对象本身. 首先我们开了 开发人员 类,使自我等于 开发人员 class. 接下来,我们做 class << 自我,使自己等于 开发人员的元类. 然后定义一个方法 后端 on 开发人员的元类.

class << 开发人员
  def的后端
    “我是后端开发人员”
  结束
结束

通过定义这样的块,我们正在设置 自我 to 开发人员块期间的元类. 结果, 后端 方法添加到 开发人员的元类,而不是类本身.

让我们看看这个元类在继承树中的行为:

继承树中的元类

正如您在前面的示例中看到的,甚至没有真正的证据证明元类存在. 但是我们可以用一个小技巧告诉我们这个不可见类的存在:

类对象
  def 元类_example
    class << 自我
      自我
    结束
  结束
结束

中定义一个实例方法 Object 类(是的, 我们可以随时重新上课, 这是元编程的另一个美妙之处), 我们会有一个 自我 参考 Object 里面的对象. 然后我们可以使用 class << 自我 语法更改当前的 自我 指向当前对象的元类. 因为当前对象是 Object 类本身,这将是实例的元类. 方法返回 自我 在这一点上,哪个是元类本身. 通过在任何对象上调用这个实例方法我们可以得到那个对象的元类. 让我们来定义 开发人员 然后开始探索

类开发人员

  def前端
    P”内部实例方法,自我是:“+ 自我.to_s
  结束

  class << 自我
    def的后端
      P”里面的类方法,自我是:+ 自我.to_s
    结束
  结束
  
结束

开发商.新
开发人员.前端
# "inside instance method, 自我 is: #<开发人员:0x2ced3b8>"

开发人员.后端
# "内部类方法,自我是:开发人员"

在元类中,自我是:+ 开发人员.元类_example.to_s
# "inside 元类, 自我 is: #>"

为了达到高潮,我们来证明一下 前端 是类的实例方法和 后端 是元类的实例方法:

p开发人员.class.instance_methods假
#(前端):

p开发人员.class.元类_example.instance_methods假
#(后端):

不过,要获得元类,您不需要真正重新打开 Object 然后加上这个hack. 你可以用 singleton_class Ruby提供的. 它是一样的 元类_example 通过这个hack,你可以看到Ruby是如何工作的:

p开发人员.class.singleton_class.instance_methods假
#(后端):

使用" class_eval "和" instance_eval "定义方法

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

类开发人员
结束

开发人员.instance_eval做
  P "instance_eval - 自我是:" + 自我.to_s
  def的后端
    P "在方法中自我是:" + 自我.to_s
  结束
结束
# "instance_eval - 自我是:开发人员"

开发人员.后端
# "方法内部的自我是:开发人员"

Ruby解释器在实例的上下文中计算这段代码,在本例中为a 开发人员 object. 当你在一个对象上定义一个方法时,你要么创建一个类方法,要么创建一个单例方法. 在本例中,确切地说,它是一个类方法, 类方法是单例方法,但是是类的单例方法, 而其他都是对象的单例方法.

另一方面, class_eval 在类而不是实例的上下文中计算代码. 它实际上重新开启了课堂. 下面是方法 class_eval 可以用来创建实例方法:

开发人员.class_eval做
  P "class_eval - 自我是:" + 自我.to_s
  def前端
    P "在方法中自我是:" + 自我.to_s
  结束
结束
# "class_eval - 自我是:开发人员"

发展商.新
# #<开发人员:0x2c5d640>

开发人员.前端
# "inside a method 自我 is: #<开发人员:0x2c5d640>"

总而言之,当你打电话时 class_eval 方法,你改变 自我 来引用原始类和调用时 instance_eval, 自我 更改为引用原始类的元类.

动态定义丢失的方法

元编程的另一个难题是 method_missing. 在对象上调用方法时,Ruby首先进入类并浏览其实例方法. 如果在那里没有找到方法,则继续向上搜索祖先链. 如果Ruby仍然没有找到该方法,它调用另一个名为 method_missing 的实例方法是什么 内核 每个对象都继承. 因为我们确信Ruby最终会为丢失的方法调用这个方法, 我们可以用它来实现一些技巧.

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

类开发人员
  定义方法:前端做|*my_arg|
    my_arg.注入(1:*)
  结束

  class << 自我
    def create_后端
      singleton_class.S结束 (:使用define_method, "后端") do
        “从灰烬中诞生!"
      结束
    结束
  结束
结束

开发商.新
p开发人员.前端(2,5,10)
# => 100

p开发人员.后端
#未定义的方法'后端'为开发人员:类(NoMethodError)

开发人员.create_后端
p开发人员.后端
#“浴火重生!"

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

类开发人员

  def coding_前端
    P“写作前端”
  结束

  def coding_后端
    P“写后端”
  结束

结束

开发商.新

开发人员.coding_前端
# "writing 前端"

开发人员.coding_后端
#“写后端”

这段代码不是干,而是使用 使用define_method 我们可以把它做成干的。

类开发人员

  (“前端”、“后端”).每个do |方法|
    定义方法“coding_#{方法}
      P“写作”+方法.to_s
    结束
  结束

结束

开发商.新

开发人员.coding_前端
# "writing 前端"

开发人员.coding_后端
#“写后端”

这好多了,但还不够完美. 为什么? 如果我们想添加一个新方法 coding_debug 例如,我们需要把这个 “调试” 放入数组中. 但使用 method_missing 我们可以解决这个问题:

类开发人员

  *args; &块
    返回超方法,*args, &阻塞除非方法.To_s =~ /^coding_\w+/
    自我.class.S结束 (:使用define_method, method
      P“写作”+方法.to_s.gsub (/ ^ coding_ /”).to_s
    结束
    自我.发送方法,*args, &块
  结束

结束

开发商.新

开发人员.coding_前端
开发人员.coding_后端
开发人员.coding_debug

这段代码有点复杂,让我们来分解一下. 调用一个不存在的方法会被触发 method_missing. 在这里,我们只希望在方法名以开头时创建新方法 “coding_”. 否则,我们只需调用super来报告实际丢失的方法. 我们只是简单地使用 使用define_method 来创建那个新方法. 就是这样! 使用这段代码,我们可以创建数以千计的新方法 “coding_”,这就是为什么我们的代码是干. 自 使用define_method 碰巧是私人的 模块,我们需要使用 发送 调用它.

结束

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

希望本文可以帮助您更深入地理解元编程,甚至构建自己的DSL, 您可以使用哪个来更有效地编写代码.

就这一主题咨询作者或专家.
预约电话
尼古拉·托多罗维奇的头像
尼古拉Todorovic
14 的经验

位于 贝尔格莱德,塞尔维亚

成员自 2015年8月4日

作者简介

Nikola拥有MCE学位和近十年的软件开发经验. 他的爱好是Ruby on Rails和创业.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.