内容简介:许多编程语言都会附带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 复制代码
binding
与 eval
在当前的作用域中都是私有方法
> 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_eval
跟 module_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" 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Map 大家族的那点事儿 ( 7 ) :ConcurrentHashMap
- Map大家族的那点事儿(4) :HashMap – 解决冲突
- Map大家族的那点事儿(4) :HashMap – 为什么是hash?
- “乱世”木马家族分析报告
- KrakenCryptor勒索家族分析报告
- Linux流行病毒家族&清除方法集锦
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。