Metaprogramming 聽起來很花哨,但它好嗎?
它可能有用,但許多人沒意識到使用 metaprogramming 是有代價的。
先讓我們在同一頁面上……
Metaprogramming 到底是什麼?
我將 metaprogramming 定義為使用任何方法:
- 改變代碼結構(如
define_method) - 將字符串作為實際 Ruby 代碼運行(如
instance_eval) - 對某些事件做出反應(如
method_missing)
那麼 metaprogramming 的代價是什麼?我將它們分為 3 組:
- 速度
- 可讀性
- 可搜尋性
注意: 你也可以說有第四組:安全性。原因是
eval方法,它們不會對傳入的內容進行任何安全檢查。你必須自己做。
讓我們更詳細地探索每一個!
速度
第一個代價是速度,因為大多數 metaprogramming 方法比常規方法慢。
這裡有一些基準測試代碼:
require 'benchmark/ips' class Thing def method_missing(name, *args) end def normal_method end define_method(:speak) {} end t = Thing.new Benchmark.ips do |x| x.report("normal method") { t.normal_method } x.report("missing method") { t.abc } x.report("defined method") { t.speak } x.compare! end 結果 (Ruby 2.2.4):
normal method: 7344529.4 i/s defined method: 5766584.9 i/s - 1.34x slower missing method: 4777911.7 i/s - 1.54x slower
如你所見,兩種 metaprogramming 方法(define_method 和 method_missing) 都比普通方法慢不少。
這裡有個有趣的發現……
上面的結果來自 Ruby 2.2.4,但如果你在 Ruby 2.3 或 Ruby 2.4 上運行這些基準測試,似乎這些方法變得更慢了!
Ruby 2.4 基準測試結果:
normal method: 8252851.6 i/s defined method: 6153202.9 i/s - 1.39x slower missing method: 4557376.3 i/s - 1.87x slower
我運行了這個基準測試好幾次以確保這不是偶然。
但如果你注意並看看每秒迭代次數 (i/s),似乎常規方法從 Ruby 2.3 開始變快了。這就是 method_missing 看起來慢得多的原因 🙂
可讀性
使用 instance_eval / class_eval 方法時,錯誤信息可能不太有幫助。
看看下面的代碼:
class Thing class_eval("def self.foo; raise 'something went wrong'; end") end Thing.foo 這將導致以下錯誤:
(eval):1:in 'foo': 'something went wrong...' (RuntimeError)
注意我們缺少文件名(它說 eval 代替)和正確的行號。好消息是有修復方法,這些 eval 方法接受兩個額外參數:
- 文件名
- 行號
使用內建常量 __FILE__ 和 __LINE__ 作為 class_eval 的參數,你將在錯誤信息中獲得正確信息。
例子:
class Thing class_eval( "def foo; raise 'something went right'; end", __FILE__, __LINE__ ) end
為什麼這不是預設值?
我不知道,但如果你打算使用這些方法,這是值得記住的 🙂
可搜尋性
Metaprogramming 方法使你的代碼搜尋性較差、文檔較難訪問(通過較差的文檔)且更難調試。
如果你在尋找方法定義,你無法用 CTRL+F(或你使用的任何快捷鍵)找到通過 metaprogramming 定義的方法,特別是如果方法名是在運行時構建的。
下面的例子使用 metaprogramming 定義了 3 個方法:
class RubyBlog def create_post_tags types = ['computer_science', 'tools', 'advanced_ruby'] types.each do |type| define_singleton_method(type + "_tag") { puts "This post is about #{type}" } end end end rb = RubyBlog.new rb.create_post_tags rb.computer_science_tag 生成文檔的工具(如 Yard 或 RDoc) 找不到這些方法並列出它們。
這些工具使用稱為"靜態分析"的技術來尋找類和方法。這項技術只能找到直接定義的方法(使用 def 語法)。
試著用最後一個例子運行 yard doc,你會看到找到的唯一方法是 create_post_tags。
它看起來像這樣:

有一種方法告訴 yard 記錄額外的方法,使用 @method 標籤,但這並不總是實用。
例子:
class Thing # @method build_report define_method(:build_report) end
另外如果你要用 grep、ack 或編輯器搜尋方法定義,找 metaprogramming 方法會比常規方法難得多。
「我不認為 Sidekiq 使用任何 metaprogramming,因為我發現它在 95% 的情況下比幫助更能模糊代碼。」 — Mike Perham, Sidekiq 創始人
結論
Metaprogramming 不全是壞事。在正確的情況下它可能有用,使你的代碼更靈活。
只要意識到額外的代價,這樣你就能做出更好的決策。
如果你覺得有用別忘了 分享這篇文章 🙂