Ruby動態刪除方法中的列印語句
0. 前言及需求
執行時動態程式設計也算是Ruby的賣點之一,心血來潮想要嘗試一下我是否能夠在執行時動態對Ruby原有的方法進行調整?可能生產線上用不上這些黑科技,不過對於個人折騰來說這還是蠻好的,可以打發一下單身的時光。
我們設定一個簡單的方法,做的事情很簡單,只是輸出了對應的方法名,並且前後有形如p xxx
的列印語句
def method_with_print
p "begin method"
puts "This is '#{self.method(:method_with_print).name}' method"
p "end method"
end
if __FILE__ == $0
method_with_print
end
執行結果如下
>> ruby xx.rb
"begin method"
This is 'method_with_print' method
"end method"
我們有什麼辦法把上述方法中的p
語句去掉,我能想到的方式是
- 獲取方法的字串版本。
- 使用正規表示式匹配並替換
p
語句,得到一個新的定義方法的字串。 - 在恰當的上下文執行新的字串,重新定義方法。
- 以Ruby的方式包裝一個方法,去改造並呼叫原來的方法。
看起來好像也不難,一步步來試試看。
1. 獲取方法定義的字串版本
如果我們是用的JavaScript,想要獲取對應方法的字串版本只需要呼叫方法物件的toString
便可
> function a() {
... return "lan"
... }
undefined
> a.toString()
'function a() {\nreturn "lan"\n}'
但Ruby似乎並沒有這麼直接的方式獲取定義方法的字串,估計是社群覺得這種需求其實用處不大吧,從網上搜尋了一下有一個叫做method_source的包可以做到這一點,他是以補丁的方式來增強Ruby原有的模組方法,進而為每一個非繫結方法新增一個source
的屬性,我們通過這個屬性就可以獲取到方法的原始碼了。不過這裡有一個問題,這個方法的實現機制是通過呼叫目標方法原有的source_location
方法獲取到定義該方法的具體位置,然後訪問對應的檔案,擷取出指定方法對應的定義字串。換句話說如果我們的方法是在REPL 裡面定義的話就不能獲取到對應方法的字串了。目前我們只能在獲取在指令碼里(xx.rb)
定義對應方法的字串了。
上面所說的這個庫,其實已經包含在我們平時用得比較多的pry工具裡面了,這是一個比較常用的REPL工具,現在我只需要在原有程式碼的可執行部分裡面新增
if __FILE__ == $0
# method_with_print
require 'pry'
puts Object.instance_method(:method_with_print).source
end
執行對應指令碼 xx.rb
就能得到目標方法的定義字串了
>> ruby xx.rb
def method_with_print
p "begin method"
puts "This is '#{self.method(:method_with_print).name}' method"
p "end method"
end
可以看到以上的做法相比JavaScript來說有點繞,因為Ruby的方法是不需要新增括號就可以呼叫的,直接寫方法名的話就是呼叫方法。這裡想要操作對應方法名的方法物件,並且是非繫結版本的。可以通過Object.instance_method
來獲取。
2. 正則匹配且替換
我們已經獲取了對應的字串版本了,那麼接下來要做的就是匹配並且替換掉原來的p xxxx
語句了。
我就寫一個比較簡單的正則匹配就好了,畢竟如果要匹配ruby所有的列印語句的話,會佔用比較多的時間以及篇幅。
根據上面的思路我得到了這樣一個程式版本
if __FILE__ == $0
# method_with_print
require 'pry'
REG_CONSOLE = /\s+p\s+.+/
method_string = Object.instance_method(:method_w\
ith_print).source
method_string.gsub!(REG_CONSOLE, '')
method_string
end
執行看看
>> ruby xx.rb
def method_with_print
puts "This is '#{self.method(:method_with_print).name}' method"
end
可見,對應的p xxx
語句已經從字串中刪除了,我們已經得到了改版之後的方法定義字串了。
3. 重新定義方法
如何重新定義方法? 我們應該都聽過JavaScript有名為eval
的方法,可以動態執行字串。類似的的Ruby也有Kernel#eval
。而且它在類層面還提供了Class#class_eval
,在物件層面提供了Object#instance_eval
方法,讓你可以操作不同的上下文。這裡講一下比較直觀的Kernel#eval
, 我們要直接執行Ruby程式碼可以像這樣執行
>> eval("p 'I love ruby'")
"I love ruby"
=> "I love ruby"
那之前定義的方法是不是也能以字串的形式,通過Kernel#eval
方法來重新定義?我把程式碼寫成這樣
if __FILE__ == $0
# method_with_print
require 'pry'
REG_CONSOLE = /\s+p\s+.+/
method_string = Object.instance_method(:method_with_print).source
method_string.gsub!(REG_CONSOLE, '')
eval(method_string)
method_with_print
end
執行看看結果是否符合預期
>> ruby xx.rb
This is 'method_with_print' method
Awesome, 已經滿足了我們這次的需求了,我們可以在執行時刪除方法的p xxx
語句,並且重新定義了原有的方法。最後我試試用Ruby的方式來處理一下個問題,肯定不是最優雅的,不過這是我目前能想到的足夠折騰的處理方式。
4. Ruby的處理方式
Ruby是“真”物件導向的程式語言,因為他真的能夠做到一切都是物件,比如
[1] pry(main)> 1.to_s
=> "1"
[2] pry(main)> '2'.to_i
=> 2
平時我們定義的函式,其實也是方法
[3] pry(main)> def m
[3] pry(main)* 'lan'
[3] pry(main)* end
=> :m
[4] pry(main)> self.m
=> "lan"
[5] pry(main)> self
=> main
m
其實是掛在main
這個物件上的方法。用物件導向的方式來解決上面的問題,我們是否可以給方法新增一個屬性,通過這個屬性來呼叫原有方法的刪除了p xxxx
語句之後的版本呢?我們首先來看看方法物件的繼承鏈條
[6] pry(main)> m_method = Object.instance_method(:m)
[10] pry(main)> m_method.class.ancestors
=> [UnboundMethod,
MethodSource::MethodExtensions,
MethodSource::SourceLocation::UnboundMethodExtensions,
Object,
PP::ObjectMixin,
Kernel,
BasicObject]
可見方法物件所屬類的祖先鏈如下
[UnboundMethod, MethodSource::MethodExtensions, MethodSource::SourceLocation::UnboundMethodExtensions, Object, PP::ObjectMixin, Kernel, BasicObject]
祖先鏈有這一大堆的東西,那要不我們就斗膽一點擴充套件一下MethodSource::MethodExtensions
這個模組吧。 你怎麼知道他是一個模組而不是類?
[13] pry(main)> MethodSource::MethodExtensions.class
=> Module
我嘗試在模組裡面新增MethodSource::MethodExtensions#remove_p_statement
方法
require 'pry'
module MethodSource::MethodExtensions
REG_CONSOLE = /\s+p\s+.+/
def remove_p_statement(*params)
method_string = self.source.gsub(REG_CONSOLE, '')
method_owner = self.owner
new_method = method_owner.instance_eval(method_string)
method_owner.send(new_method, *params)
end
end
它是方法的方法,只需要在方法的後面呼叫它。它會在物件的上下文Object#instance_eval
重新定義這個方法,然後在內部自動發派這個方法,並附帶上一個可變引數。最後我把執行指令碼的主體內容改為
if __FILE__ == $0
puts "=========="
puts "new method result:\n"
Object.instance_method(:method_with_print).remove_p_statement()
puts "=========="
puts "=========="
puts "old method result:\n"
method_with_print()
puts "=========="
end
PS: 由於
method_with_print
方法定義的時候沒有引數,我們這裡括號裡面的內容都是空。
最後的結果如下
==========
new method result:
This is 'method_with_print' method
==========
==========
old method result:
"begin method"
This is 'method_with_print' method
"end method"
==========
可見,我們的方法呼叫了 MethodSource::MethodExtensions#remove_p_statement
這個方法之後得到了一個新的方法並執行,但是卻不會影響到執行指令碼上下文中最初定義的原始方法的行為。
5. 再見
以上程式碼有什麼用? .........其實還真沒什麼卵用,純屬瞎折騰。
Happy Coding and Writing !!!
相關文章
- 動態sql語句來刪除使用者下的物件SQL物件
- excel中的列印線如何刪除Excel
- MyBatis刪除多行,in語句的使用MyBatis
- 批處理刪除語句
- 英語的靜態句和動態句
- 用SQL語句刪除重複記錄的四種方法SQL
- springboot 中列印 sql 語句Spring BootSQL
- Android中ListView動態新增刪除項AndroidView
- js動態新增、刪除table中的tr、td、inputJS
- Laravel 中除錯輸出 SQL 語句的簡便方法Laravel除錯SQL
- 常見的SQL語句(建立、刪除、切換)SQL
- Ruby如何實現動態方法呼叫
- 用Java中for迴圈語句列印菱形Java
- ABAP 動態where語句
- 關於動態語句
- 用動態SQL語句SQL
- 動態SQL語句 (轉)SQL
- ORACLE 動態語句的筆記Oracle筆記
- 用SQL語句增加刪除修改欄位SQL
- MySQL建立索引、修改索引、刪除索引的命令語句MySql索引
- [Object-C語言隨筆之二] 《NSLog》常用的列印除錯語句與自動排版ObjectC語言除錯
- layui動態新增刪除表格,並獲取表格中的值UI
- 6、MySQL刪除資料庫(DROP DATABASE語句)MySql資料庫Database
- 簡單查詢、插入、更新、刪除SQL語句SQL
- PLSQL_動態語句的解析(概念)SQL
- ibatis列印sql語句BATSQL
- SQL SERVER 中構建執行動態SQL語句SQLServer
- JavaScript 動態新增與刪除元素JavaScript
- jQuery列表動態增加和刪除jQuery
- C#中刪除DataTable中的行的方法C#
- 恢復Oracle資料庫誤刪除資料的語句Oracle資料庫
- SQL Server語句刪除帶有預設值的欄位SQLServer
- Oracle 查詢並刪除重複記錄的SQL語句OracleSQL
- Mybatis 動態執行SQL語句MyBatisSQL
- day06-動態SQL語句SQL
- PL/SQL 動態sql語句例SQL
- 分支、迴圈語句動態展示
- Ruby動態類別