Eval家族的那些事儿

栏目: Ruby · 发布时间: 5年前

内容简介:许多编程语言都会附带eval的功能,通常会出现在动态语言中,它就有点像是一个微型的解释器,可以在运行时解释代码片段。这篇文章主要以Ruby为例,详细介绍Ruby中的Eval是Ruby语言中比较有意思的一个功能了。其实不仅仅是Ruby,许多语言都开放了这个功能。不过在不同语言里,该功能的命名方式以及侧重点会有所不同。在Lua编程语言中,eval的功能通过一个叫

许多编程语言都会附带eval的功能,通常会出现在动态语言中,它就有点像是一个微型的解释器,可以在运行时解释代码片段。这篇文章主要以 Ruby 为例,详细介绍Ruby中的 eval 家族。

代码片段的执行者eval

Eval是Ruby语言中比较有意思的一个功能了。其实不仅仅是Ruby,许多语言都开放了这个功能。不过在不同语言里,该功能的命名方式以及侧重点会有所不同。

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 的函数来实现,用起来就像是一个简单的REPL

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脚本文件

// 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#binding与 Kernel#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"
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序开发心理学

程序开发心理学

(美)杰拉尔德·温伯格 / 邓俊辉 / 清华大学出版社 / 2004-1-1 / 39.00元

本书开创"以人为本"研究方法的先驱,在长达25年的岁月中一直保持活力,至今仍在继续。1997年,本书作者温伯格因其在软件领域的杰出贡献,被美国计算机博物馆的计算机名人堂选为首批5位成员之一。 在计算机界,还没有任何一本计算机方面的书,在初次出版之后,能够在长达25年的岁月中一直保持活力--而且这种活力到今天仍在继续。《程序开发心理学》是开创"以人为本"研究方法的先驱,它以其对程序员们在智力、......一起来看看 《程序开发心理学》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具