《Ruby元编程》第6章编写代码的代码的学习笔记,主要内容是eval和钩子方法。
从需求说起
本章开始用一个boss提出的问题作为引子,我觉得这个例子很好。简述下问题:
创建一个名为attr_checked的类宏,这个类宏要满足两个条件:
- 接受属性名和代码块,代码块用于验证属性值的有效性
- 只有当类包含某个模块,比如checkedAttributes时,才可以使用attr_checked
解决思路可以转化成:
- 如何定义一个类宏方法?【先用eval定义一个内核方法,然后再改造成类宏】
- 如何给一个类宏方法添加代码块?【method添加&block参数即可】
- 定义模块checkedAttributes,通过钩子方法为指定的类添加attr_checked
认识下eval
定义自己的类宏方法前,先来认识下Kernel#eval。
eval是内核方法,参数是一段ruby代码文本。相比instance_eval, class_eval,它只能执行代码字符串。那instance_eval, class_eval是否能执行代码字符串?可以。
看个例子:
my_array = [1,2,3]
my_array.instance_eval "self.reduce(&:+)" #= > 6
eval("my_array.inject {|sum, x| sum + x}") #= > 6
这类代码字符串可以携带一个binding对象,然后在该对象的作用域中执行代码。
eval与binding
Binding是一个用对象表示的作用域,可以通过Kernel#binding 来创建。ruby 还提供了TOPLEVEL_BINDING的预定义常量,表示顶级作用域的Binding对象。
看代码理解下:
class MyClass def my_method @x = 1 binding end end obj = MyClass.new.my_method eval("@x",obj) #= > 1 eval("self", TOPLEVEL_BINDING) #= > main
eval的麻烦
最大的问题是安全性,因为执行的是一段字符串,ruby是不会对字符串进行语法检查的,容易引发代码注入攻击。
看个例子:
def explore_array(method) code = "['a','b','c'].#{method}" eval code end explore_array("include?('a')") #= > true explore_array("size") #= > 3 explore_array("object_id; Dir.glob('*')") #= > 列出了该文件所在目录下的所有文件
这里,运行类似
explore_array("object_id; Dir.glob('*')")
这样的恶意代码可能会带来意想不到的后果。所以,能用代码块就用代码块。如何防止代码注入攻击?
解析所有的代码字符串,不现实
改用动态方法和动态派发来替换eval
比如上面的例子,explore_array可以这样写:
def explore_array(method, *arguments) ['a','b','c'].send(method, *arguments) end explore_array(:include?, 'a') #= > true explore_array(:size) #= > 3
污染对象和安全级别
针对eval存在的安全问题,可以采用一些使它变得安全的方法。
Ruby会自动把不安全的对象标记为污染对象,你可以通过给$SAFE赋值([0,1, 2,3])来设置安全级别,禁止某些潜在的危险操作,其中0最低,3最高。
通过使用安全级别,可以为eval方法创造了一个可控的环境,像这样的环境称之为沙盒。
用eval来定义add_checked_attr
「其实,这里我并不是很理解,为什么要调用eval来定义这个method,然后又去掉它?为什么不直接用class_eval的方式来定义?难道这样绕一圈,仅仅是为了向读者介绍eval?」
不吐糟,直接看解答:
def add_checked_attr(klass, attr)
eval "
class #{klass}
def #{attr}=(value)
raise 'Invalid attribute' unless value
@#{attr} = value
end
def #{attr}
@#{attr}
end
end
"
end
去掉eval,加上block
鉴于eval的问题,改用class_eval来打开类,同时给方法添加一个block
def add_checked_attr(klass, attr, &validation)
klass.class_eval do
define_method "#{attr}=" do |value|
raise 'Invalid attribute' unless validation.call(value) # validation 是一个proc
instance_variable_set("@#{attr}", value)
end
define_method attr do
instance_variable_get("@#{attr}")
end
end
end
改造成类宏
为了让add_checked_attr对所有类都可用,可以将它定义在class中,另修改方法名为attr_checked,去掉类参数。
解答如下:
class Class
def attr_checked(attr, &validation)
define_method "#{attr}=" do |value|
raise 'Invalid attribute' unless validation.call(value) # validation 是一个proc
instance_variable_set("@#{attr}", value)
end
define_method attr do
instance_variable_get("@#{attr}")
end
end
end
最后请上我们的hooks。
钩子方法
看个例子:
module M
def self.included(othermod)
puts "M was included into #{othermod}"
end
end
class C
include M
end
#= > M was included into C
Module#included在一个module被include时,被自动调用,这种方法称之为钩子方法,因为它们像钩子一样,钩住一个特定的事件。类似的方法还有Module#method_added, Module#extend_object等。
想要捕获单件方法的钩子事件,需要使用BasicObject的singleton_xxx方法,比如singleton_method_added。
针对上面的例子,可以覆写include方法得到一样的效果:
module M;end
class C
def self.include(*modules)
puts "#{modules} was included into C"
super
end
include M
end
#= > [M] was included into C
注意这里覆写include中,最后调用了super,不然这个module是不会被include的。这种通过覆写添加额外功能的效果,也可以通过环绕别名的方式实现。
包含并扩展类的方式就用到了钩子,看个例子:
module M
def self.included(klass)
klass.extend ClassMethods
end
end
module ClassMethods
def my_method
puts "it could be a class method"
end
end
class D
include M
end
D.my_method #= > it could be a class method
类D包含了模块M,调用了M的钩子方法:included, 这个钩子方法接着用module ClassMethods扩展了类D,使得module ClassMethods的my_method 成为了类D的类方法。
Rails中ActiveSupport有一个module Concern,就封装了包含并扩展的技巧。
使用这种技巧,完成最后一步。
module CheckedAttributes
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def attr_checked(attr, &validation)
define_method "#{attr}=" do |value|
raise 'Invalid attribute' unless validation.call(value) # validation 是一个proc
instance_variable_set("@#{attr}", value)
end
define_method attr do
instance_variable_get("@#{attr}")
end
end
end
end
OK !
后记
整本书到这里,基本算是走完了大半了,后面第二部分是有关rails中的元编程,篇幅就相对而言少了些,作者挑了几个板块说一下。我的感觉是,如果前面的六章理解的七七八八了,可以看一些gem的源代码,有利于加深理解。你会在其中发现很多元编程的身影。