笔记:Ruby元编程实验

导航关联

Ruby特性和实验分析汇总

eval

# 文档地址
# https://devdocs.io/ruby~3/kernel#method-i-eval
# eval(string [, binding [, filename [,lineno]]]) → obj 

top_a = 'top_a var'

class A
  class_a_inner_var = 'class a inner var'
  eval("puts class_a_inner_var")
  eval("pp local_variables")
end

# output >>>>>
# class a inner var
# [:class_a_inner_var]

eval的特点是接受字符串,并且携带当前绑定。

我也可以用不定长字符串


# https://devdocs.io/ruby~3/kernel#method-i-eval
# eval(string [, binding [, filename [,lineno]]]) → obj 

top_a = 'top_a var'

class A
  method_name = 'run'

  eval <<-DEF_FUNC
    define_method "hello_#{method_name}".to_sym do
      puts method_name
    end
  DEF_FUNC
end

A.new.hello_run

# output >>>
# run

这里可能只能用 define_method 来写,因为我们这里使用了变量。

eval & binding

binding 就是一个用对象表示的完整作用于,eval的第二个参数是 binding对象,我们甚至可以用这个这种方式,像剪切一样的转移作用域。



class A

  def my_method
    @x=1
    
    binding
  end
end


binding_obj = A.new.my_method

eval "puts @x", binding_obj

# output >>>>
# 1

缺点

eval的缺点在于安全性和容易代码注入。所以只运行你信任的来源,或者禁用eval,这也很常见。

替代eval

  1. 动态定义方法 define_method
  2. 动态派发方法 xxxx.send

eval远亲

执行文件内容

  1. Kernel#load
  2. Kernel#lrequire

这两个很像eval,读取文件并且执行。

instance_eval

class A
  attr_reader :name
  def initialize
    @name = 'A name'
  end
end

top_name = 'top name'
obj = A.new

obj.instance_eval do
  pp self
  pp local_variables
  puts "#{@name} / #{top_name} / #{self.name}"
end

# output >>>>>

# #<A:0x00007fd16e872110 @name="A name">
# [:top_name, :obj]
# A name / top name / A name

instance_eval 打开了 obj的内部,我们可以 通过实例变量访问内部,同时他也连接了所在词法作用域。

instance_eval 内部打印self,self指向该实例。

所以 instance_eval 可以作为一个上下文探针,可以通过它获得我们关心的对象内部情况。

instance_exec

instance_exec 是 instance_eval的孪生兄弟,它允许向 块 传入参数。


A.new.instance_exec(some_arg) { |y| # do sth }

技巧: 洁净室&白板类

因为Object有很多方法(大概个方法58左右 Object.instance_methods.count)。而作为块一旦绑定了词法作用于之后,一些代码和变量可能会和环境冲突。

因此,我们有时候需要一个环境,来执行我们想要的代码,这个就叫做洁净室。

BasicObject(只有8个方法左右 BasicObject.instance_methods.count) 可以堪此重任,因为他几乎没有什么方法。


备注:这里可能有人会懵逼 —— 我为什么要关心这个

因为作用域在Ruby里是可控的存在。可以参考 6-Ruby和JavaScript横向对比 实际上Ruby里面要自己去控制作用域,灵活的取用 块 的写法,来控制可见什么。 而且Ruby的方法是自动在作用域里搜寻的。那么我们控制了作用域,意味着控制着我们的程序会搜索到哪个有效方法。 于是我们有一份的工作是 控制谁可以搜到,谁最好不要搜到。所以才有这里面的技巧。



BasicObject.new.instance_eval do
  # 洁净室
  # 在这里运行你感兴趣的代码
  
  # 这里连常量都不在没有,使用常量需要绝对路径  ::Stirng
end

类宏 (Class Marco)

相关参考

class_eval

Module#class_eval (别名是module_eval)

Module#class_eval 和 Object#instance_eval(《Ruby元编程》85页) 截然不同。instance_eval 只会修改 self,class_eval 会同时修改 self和当前类。

(instance_eval 修改当前类在 《Ruby元编程》127页)

通常我们用了instance_eval 打开非类对象,class_eval打开类对象。


class A

end

A.class_eval do
  def new_method
    # 定义方法
    puts "new method call"
  end
end

A.new.new_method

# output >>>
# new method call

Module#class_eval 可以打开 Class的作用域 添加方法。


评论

Ruby里面同源同根的方法太多,这里面的实验只是列举了一些而已,而实际上可以查看标准库可以获得更多。

那么多的方法,他们都遵循了一些规律。我们可以用已经学会的东西入手搜索。

这里比如 instance_eval 和 class_eval 都可以改变 self(同源方法),我们的使用更要小心,我们需要用代码来表达自己的意图。所以实例、类分开使用比较合理。

Ruby中有大量的方法,需要依靠语义来支撑上下文。否则就会很容易陷入动态陷阱。

并且还要保持好的习惯,让代码表达出意图——在该利用搜索的地方搜索,在不详搜索的地方就应该利用技巧关闭作用域。

Mark24

Everything can Mix.