Eval家族的那些事兒

許多編程語言都會附帶eval的功能,一般會出如今動態語言中,它就有點像是一個微型的解釋器,能夠在運行時解釋代碼片斷。這篇文章主要以Ruby爲例,詳細介紹Ruby中的eval家族。html

代碼片斷的執行者eval

Eval是Ruby語言中比較有意思的一個功能了。其實不只僅是Ruby,許多語言都開放了這個功能。不過在不一樣語言裏,該功能的命名方式以及側重點會有所不一樣。python

在Lua編程語言中,eval的功能經過一個叫load(版本5.2以後)的函數來實現,不過它解釋完代碼片斷以後會返回一個新的函數,咱們須要手動調用這個函數來執行對應的代碼片斷程序員

> load("print('Hello World!')")()
Hello World!
複製代碼

詭異的地方在於,它不能解析單純的算術運算編程

> 1 + 2
3

> load("1 + 2")()
stdin:1: attempt to call a nil value
stack traceback:
	stdin:1: in main chunk
	[C]: in ?
複製代碼

要解析算術運算,須要把它們包裝成方法體安全

> f = load("return 1 + 2")
> f()
3
複製代碼

在Python中該功能是經過名爲eval的函數來實現,用起來就像是一個簡單的REPLruby

In [2]: eval
Out[2]: <function eval>

In [3]: eval('1 + 2')
Out[3]: 3

In [4]: eval('hex(3)')
Out[4]: '0x3'
複製代碼

不過奇怪的地方在於它不能直接解析Python中語句,好比說print語句編程語言

In [5]: eval('print(1 + 2)')
  File "<string>", line 1
    print(1 + 2)
        ^
SyntaxError: invalid syntax
複製代碼

要打印東西,能夠考慮把上述語句封裝成一個方法編輯器

In [12]: def c_print(name):
   ....:     print(name)
   ....:

In [13]: eval("c_print(1 + 2)")
3
複製代碼

相比之下,Ruby的eval彷佛就沒節操得多,或許是由於借鑑了Lisp吧?它幾乎能執行任何代碼片斷函數

> eval('print("hello")')
hello => nil
> eval('1 + 2')
 => 3
複製代碼

接下來我嘗試用它來執行腳本文件中的代碼片斷,假設我有這樣一個Ruby腳本文件oop

// example.rb
a = 1 + 2 + 3

puts a
複製代碼

想要執行這個文件,最直接的方式就是

ruby example.rb

6
複製代碼

然而你還能夠經過eval來作這個事情

> content = File.read("example.rb") # 讀取文件中的代碼片斷
 => "a = 1 + 2 + 3\n\nputs a\n"
> eval(content)
6
 => nil
複製代碼

固然Ruby中的eval毫不僅如此,且容我慢慢道來。

Eval與上下文

在Ruby中用eval來執行代碼片斷的時候會默認採用當前的上下文

> a = 10000
=> 10000
> eval('a + 1')
=> 10001
複製代碼

咱們也能夠手動傳入當前上下文的信息,故而,如下的寫法是等價的

eval('a + 1', binding)
=> 10001
複製代碼

bindingeval在當前的做用域中都是私有方法

> self
 => main
> self.private_methods.grep(:binding)
=> [:binding]
> self.private_methods.grep(:eval)
=> [:eval]
複製代碼

在功能上,它們分別來自於Kernel#bindingKernel#eval

> Kernel.singleton_class.instance_methods(false).grep(:eval)
 => [:eval]
> Kernel.singleton_class.instance_methods(false).grep(:binding)
=> [:binding]

> Kernel.eval('a + 1', Kernel.binding)
=> 10001
複製代碼

有了這兩個東西,咱們能夠寫出一些比較有意思的功能。

> def hello(a)
>   binding
> end

> ctx = hello('hello world')
複製代碼
> eval('print a') # 打印當前上下文的變量`a`
10000 => nil

> eval('print a', ctx) # 打印`hello`運行時上下文的變量`a`
hello world => nil
複製代碼

經過binding截取hello方法的上下文信息並存儲在對象中,而後把該上下文傳遞至eval方法中。此外,上文的ctx對象其實也有它本身的eval方法,這是從Binding類中定義的實例方法。

> ctx.class
=> Binding

> Binding.instance_methods(false).grep /eval/
=> [:eval]
複製代碼

區別在於它是一個公有的實例方法,接收的參數也稍微有所不一樣。更簡單地咱們能夠用下面的代碼去打印hello運行時上下文參數a的值。

ctx.eval('print a')
hello world => nil
複製代碼

更多的eval變種

在Ruby中eval其實還存在一些變種,好比咱們經常使用的用於打開類/模塊的方法class_eval/module_eval其實就至關於在類/模塊的上下文中運行代碼。爲了在實例變量的上下文中運行代碼,咱們能夠採用instance_eval

a. class_eval/module_eval

在Ruby裏面類和模塊之間的關係密不可分,不少時候咱們會簡單地把模塊當作是沒法進行實例化的類,它們兩本質上是差很少的。因而乎class_evalmodule_eval兩個方法其實只是爲了讓編碼更加清晰,二者功能上並沒有太大區別。

> class A; end
 => nil
> A.class_eval "def set_a; @a = 1000; end"
 => :set_a
> A.module_eval "def set_b; @b = 2000; end"
 => :set_b
> a = A.new
 => #<A:0x00007ff59d955fc0>
> a.set_a
 => 1000
> a.set_b
 => 2000
> a.instance_variable_get('@a')
 => 1000
> a.instance_variable_get('@b')
=> 2000
複製代碼

咱們也能夠經過多行字符串來定義相關的邏輯

> A.class_eval <<M > def print_a > puts @a > end > M => :print_a > a.print_a 1000
複製代碼

不過在正式編碼環境中經過字符串來定義某些函數邏輯實在是比較蛋疼,畢竟這樣的話就沒辦法受益於代碼編輯器的高亮環境,代碼可維護性也相對下降。語言設計者或許考慮到了這一點,因而咱們能夠以代碼塊的形式來傳遞相關的邏輯。等價寫法以下

class A; end

A.class_eval do
  def set_a
    @a = 1000
  end

  def set_b
    @b = 2000
  end

  def print_a
    puts @a
  end
end

i = A.new
i.set_a
i.set_b

puts i
puts i.instance_variable_get('@a')
puts i.instance_variable_get('@b')
i.print_a
複製代碼

打印結果

#<A:0x00007fb75102cb18>
1000
2000
1000
複製代碼

與以前的例子所達成的效果是一致的,只不過寫法不一樣。除此以外,他們兩個的嵌套層級是不同的

> A.class_eval do
>   Module.nesting
> end
 => []

> A.class_eval "Module.nesting"
 => [A]
複製代碼

實際上,咱們還能夠用最開始介紹的eval方法來實現相關的邏輯

> A.private_methods.grep(:binding)
 => [:binding]
複製代碼

可見對於類A而言,binding是一個私有方法,所以咱們能夠經過動態發派來獲取類A上下文的信息。

> class_a_ctx = A.send(:binding)
=> #<Binding:0x00007f98f910ae70>
複製代碼

拿到了上下文以後一切都好辦了,可分別經過如下兩種方式來定義類A的實例方法。

> class_a_ctx.eval 'def set_c; @c = 3000; end'
=> :set_c

> eval('def set_d; @d = 4000; end', class_a_ctx)
=> :set_d
複製代碼

簡單驗證一下結果

> a = A.new
=> #<A:0x00007f98f923c078>
> a.set_d
=> 4000
> a.set_c
=> 3000
> a.instance_variable_get('@d')
=> 4000
> a.instance_variable_get('@c')
=> 3000
複製代碼

b. instance_eval

經過instance_eval能夠在當前實例的上下文中運行代碼片斷,咱們先簡單地定義一個類B

class B
  attr_accessor :a, :b, :c, :d, :e
  def initialize(a, b, c, d, e)
    @a = a
    @b = b
    @c = c
    @d = d
    @e = e
  end
end
複製代碼

實例化以後,分別用不一樣的方式來求得實例變量每一個實例屬性相加的值

> k = B.new(1, 2, 3, 4, 5)
 => #<B:0x00007f999fa2c480 @a=1, @b=2, @c=3, @d=4, @e=5>

> puts k.a + k.b + k.c + k.d + k.e
15

> k.instance_eval do
>   puts a + b + c + d + e
> end

15
複製代碼

這只是個簡單的例子,在一些場景中仍是比較有用的,好比能夠用它來定義單例方法

> k.instance_eval do
>   def sum
>     @a + @b + @c + @d + @e
>   end
> end

> k.sum
=> 15

> B.methods.grep :sum
 => []
複製代碼

我們依舊能夠採用最原始的eval方法來實現相似的功能,這裏暫不贅述。

安全問題

對於動態語言來講eval是一個很強大的功能,但隨之也帶來了很多的安全問題,最麻煩的莫過於代碼注入了。假設你的代碼能夠用來接收用戶輸入

# string_handling.rb
def string_handling(method)
  code = "'hello world'.#{method}"
  puts "Evaluating: #{code}"
  eval code
end

loop { p string_handling (gets()) }
複製代碼

若是咱們的用戶都是善意用戶的話,那沒有什麼問題。

> ruby string_handling.rb
slice(1)
Evaluating: 'hello world'.slice(1)
"e"

upcase
Evaluating: 'hello world'.upcase
"HELLO WORLD"
複製代碼

But,若是一個惡意的用戶輸入了下面的內容

slice(1); require 'fileutils'; FileUtils.rm_f(Dir.glob("*"))
複製代碼

那是否是有點好玩了?假設運行腳本的系統角色有足夠的權限,那麼當前目錄下的全部東西都會被刪除殆盡。利用動態發派來實現相似的功能或許更加安全一些

# string_handling_better.rb
def string_handling_better(method, *arg)
 'hello world'.send(method, *arg)
end
複製代碼

咱們能夠對用戶的輸入先進行預處理,而後再把它傳遞到定義好的string_handling_better方法中去。

> string_handling_better('slice', 1, 10)
 => "ello world"
複製代碼

尾聲

這篇文章分別從不一樣的角度談論了eval,以及它的一些變種。它是一個很強大的功能,不過能力越大責任越大,相應的還會帶來必定的風險,若使用不當會引起系統問題。現實編程生活當中,直接使用eval的場景並很少,畢竟代碼寫在字符串裏面的話,少了編輯器的語法高亮仍是會爲程序員帶來很多困擾。不過採用class_eval/module_eval來打開類/模塊,並以代碼塊的方式來定製邏輯的案例卻是數見不鮮。

相關文章
相關標籤/搜索