个性化阅读
专注于IT技术分析

Ruby元编程比听起来还酷

本文概述

你经常会听到元编程仅是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

这里要了解的一件事是自我的含义。前端方法是在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
end

p Developer.backend
# Developer

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

元类

Ruby中的每个对象都有其自己的元类。它在某种程度上对于开发人员是不可见的, 但是它在那里并且你可以非常容易地使用它。由于我们的开发人员类本质上是一个对象, 因此它具有自己的元类。举例来说, 我们创建一个String类的对象并操作其元类:

example = "I'm a string object"

def example.something
  self.upcase
end

p 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的元类中, 而不是类本身。

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

继承树中的元类

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

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

如果我们在Object类中定义了一个实例方法(是的, 我们可以随时重新打开任何类, 这是元编程的另一优点), 我们将拥有一个内部引用Object对象的自我。然后, 我们可以使用类<< self语法来更改当前self, 以指向当前对象的元类。由于当前对象是Object类本身, 因此它将是实例的元类。该方法返回self, 此时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>>"

对于渐进式, 让我们来看一下证明:前端是类的实例方法, 而后端是元类的实例方法:

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
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_method与method_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!"

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

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"

更好, 但仍不完美。为什么?例如, 如果要添加新的方法encoding_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_”开始的数以千计的新方法, 而这就是使我们的代码变干的原因。由于define_method恰好是Module私有的, 因此我们需要使用send来调用它。

本文总结

这只是冰山一角。要成为Ruby Jedi, 这是起点。掌握了元编程的这些构成要素并真正理解了其本质之后, 你可以进行更复杂的操作, 例如创建自己的领域特定语言(DSL)。 DSL本身就是一个主题, 但是这些基本概念是理解高级主题的前提。 Rails中一些最常用的gem是用这种方式构建的, 你甚至可能在不知道它的情况下使用了它的DSL, 例如RSpec和ActiveRecord。

希望本文可以使你更进一步地了解元编程, 甚至可以构建自己的DSL, 从而可以更有效地进行编码。

赞(0)
未经允许不得转载:srcmini » Ruby元编程比听起来还酷

评论 抢沙发

评论前必须登录!