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,你可以用它來更有效地編碼。