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来打开类/模块,并以代码块的方式来定制逻辑的案例却是数见不鲜。

相关文章
相关标签/搜索