almost 5 years ago

1. 对象模型

1.1 打开类

从某种意义上说,Ruby 的 class 关键字更像是一个作用域操作符而不是类声明语句。它的确可以创建一个还不存在的类,不过也可以把这看成一种副作用。对于 class 关键字,其核心任务是把你带到类的上下文中,让你可以在其中定义方法。

示例1:

你的代码中需要定义一个函数去掉字符串中的标点符号和特殊字符,只保留字母、数字和空格。

在像 Java 这样的编程语言中,你必须这样定义这个方法:

public class StringUtil {
    public static String to_alphanumeric(s) {
        return s.replaceAll("[^\\w\\s]", "");
    }
}

而在 Ruby 中你可以将这个 to_alphanumeric 方法直接定义为 String 类的实例方法:

class String
    def to_alphanumeric
        self.gsub /[^\w\s]/, ''
    end
end

示例2:

class D
    def x; 'x'; end
end

class D
    def y; 'y'; end
end

obj = D.new
obj.x        # => 'x'

obj.y        # => 'y'

使用“打开类”技术时,需要注意的问题:在打开一个类向其中添加一个新的方法时,需要注意这个类中是否已经存在一个同名的方法,以免不小心覆盖了类中原有的同名方法。

1.2 几个 Reflection 方法

1.2.1 Object#instance_variables()

Returns an array of instance variable names for the receiver. Note that simply defining an accessor does not create the corresponding instance variable.

1.2.2 Object#methods(all=true)

Returns a list of the names of public and protected methods of obj. This will include all the methods accessible in obj’s ancestors. If the all parameter is set to false, only those methods in the receiver will be listed.

1.2.3 Module#instance_methods(include_super=true)

Returns an array containing the names of the public and protected instance methods in the receiver. For a module, these are the public and protected methods; for a class, they are the instance (not singleton) methods. With no argument, or with an argument that is false, the instance methods in mod are returned, otherwise the methods in mod and mod’s superclasses are returned.

1.2.4 Module#ancesotrs()

Returns a list of modules included in mod (including mod itself).

class MyClass
    def my_methods; 'my_method()'; end    
end

class MySubclass < MyClass
end

MySubclass.ancestors()        # => [MySubclass, MyClass, Object, Kernel, BasicObject]

1.3 常量的作用域和路径

常量的作用域不同于变量,它有自己独特的规则。例如:

module MyModule
    MyConstant = 'Outer Constant'
    class MyClass
        MyConstant = 'Inner Constant'
    end
end

这段代码中的所有常量像文件系统一样组织成树形结构:

MyModule
├── MyClass
│   └── MyConstant
└── MyConstant

1.4 几个和常量有关的方法

1.4.1 Module#constants(inherit=true)

Returns an array of the names of the constants accessible in mod. This includes the names of constants in any included modules (example at start of section), unless the inherit parameter is set to false.

1.4.2 Module.constants()Module.constants(inherit)

In the first form, returns an array of the names of all constants accessible from the point of call. This list includes the names of all modules and classes defined in the global scope.

The second form calls the instance method constants.

1.4.3 Module.nesting()

获取当前常量的路径。例如:

moudle M
    class C
        module M2
            Module.nesting        # => [M::C::M2, M::C, M]

        end
    end
end        

1.5 修剪常量树

若想在网上找到一个 motd.rb 文件用来在控制台上显示“当天的消息”,且想把这段代码集成到最新的程序中去,那么使用load执行该文件来显示消息:

load('motd.rb')

不过,使用 load() 方法有一个副作用。motd.rb 文件很可能定义了变量和类。尽管变量在加载完成后会落在当前作用域之外,但常量不会。这样,motd.rb可能会通过它的常量(尤其是类名)污染当前程序的命名空间。

可以通过使用第二个可选参数来控制其常量仅在自身范围内有效:

load('motd.rb', true)

通过这种方式加载的文件,Ruby 会创建一个匿名模块,使用它作为命名空间来容纳 motd.rb 中定义的所有常量,加载完成后,该模块会被销毁。

require()方法与load()方法颇为相似,但是它的目的不同。通过load()方法可以执行代码,而require()则是用来导入类库。这就是require()方法没有第二个可选参数的原因。在这些类库中的类名通常是你导入这些库时所希望得到的,因此没有理由在加载后销毁它们。

1.6 关于 Kernel 模块

如果需要定义一个工具函数,这个函数的作用很广泛,以至于它使用起来更像语言内核的一部分,那么这个方法最好定义 Kernel 模块的方法。

1.7 private 究竟意味着什么

私有方法服从一个简单的规则:不能明确指定一个接受者来调用一个私有方法。换言之,每次调用一个私有方法时,只能调用于隐含的接受者 —— self 上。下面看一个极端例子:

class C
    def public_method
        self.private_method
    end
    
    private
    
        def private_method; end
end

C.new.public_method

 # => NoMethodError: private method `private_method' called […]

在这段代码中,如果去掉 self 关键字,它就可以正常运行。

这个人为制造的例子演示了私有方法是由两条规则一起控制的:第一条:如果调用方法的接受者不是你自己,则必须明确指明一个接受者;第二条,私有方法只能被隐含接受者调用。把这两条规则糅合在一起,你会发现只能在自身中调用一个私有方法。你可以把这个糅合后的规则成为“似有规则”。

1.8 混乱的模块

有如下代码:

module Printable
    def print; end
end

module Document
    def print; end
end

class Book
    include Document
    include Printable
end

book = Book.new
b.print

问:b.print 调用的是 Printable 还是 Document 中定义的 print 方法?

答:Book.ancestors 的输出:[Book, Printable, Document, Object, .. ..],因此调用的是 Printable 中定义的 print 方法。

1.9 完整的类和对象模型图

下列代码:

class MyClass
    def method_a; end
end

obj1 = MyClass.new
obj2 = MyClass.new
obj3 = MyClass.new

class << obj3
    def method_b; end
end

对应的模型图如:

2. 方法

2.1 动态方法

2.1.1 动态方法所依赖的基础
  • 动态派发:Object#send(symbol_or_string [, args…])

Invokes the method identified by symbol, passing it any arguments specified. You can use __send__ if the name send clashes with an existing method in obj. When the method is identified by a string, the string is converted to a symbol.

  • 动态定义:Module#define_method(symbol, method)Module#define_method(symbol){ block }

Defines an instance method in the receiver. The method parameter can be a Proc, a Method or an UnboundMethod object. If a block is specified, it is used as the method body. This block is evaluated using instance_eval. define_method is a private method.

2.1.2 示例

有这样一个程序:

 # data_source.rb:

class DS
    def initialzie # connect to data source…

    def get_mouse_info(workstation_id)  # …

    def get_mouse_price(workstation_id)  # …

    def get_keyboard_info(workstation_id)  # …

    def get_keyboard_price(workstation_id)  # …

    def get_cpu_info(workstatiob_id)  #…

    def get_cpu_price(workstation_id)  #…

    def get_display_info(workstation_id)  # …

    def get_display_price(workstation_id)  #...

end

 # computer.rb

class Computer
    def initialzie(id, data_source)
        @id = id
        @data_source = data_source
    end
    
    def mouse
        info = @data_souce.get_mouse_info(@id)
        price = @data_source.get_mouse_price(@id)
        result = "Mouse: #{info} ($#{price})"
        return "* #{result}" if price > 100
        result
    end
    
    def cpu
        info = @data_souce.get_cpu_info(@id)
        price = @data_source.get_cpu_price(@id)
        result = "Mouse: #{info} ($#{price})"
        return "* #{result}" if price > 100
        result   
    end
      
end  
2.1.2.1 使用动态派发重构 computer.rb
class Computer
    def initialize(id, data_source)
        @id = id
        @data_source = data_source
    end
    
    def mouse
        component :mouse
    end
    
    def cpu
        component :cpu
    end
     
    
    def component(name)
        info = @data_source.send("get_#{name}_info", @id)
        price = @data_source.send("get_#{name}_price", @id)
        result = "#{name.to_s.capitalize}: #{info} ($#{price})"
        return "* #{result}" if price > 100
        result
    end
end
2.1.2.2 使用动态定义重构 computer.rb
class Computer
    def initialzie(id, data_source)
        @id = id
        @data_source = data_source
    end

    def self.component(name)
        define_method name do 
            info = @data_source.send "get_#{name}_info", @id
            price = @data_source.send "get_#{name}_price", @id
            result = "#{name.to_s.capitalize}: #{info} ($#{price})"
            return "* #{result}" if price > 100
            result
        end
    end
    
    component :mouse
    component :cpu
     
end
2.1.2.3 使用内省重构 computer.rb
class Computer
    def initialzie(id, data_source)
        @id = id
        @data_source = data_source
        @data_source.methods.grep(/^get_(\w+)_info/) {self.class.component $1}
    end

    def self.component(name)
        define_method name do 
            info = @data_source.send "get_#{name}_info", @id
            price = @data_source.send "get_#{name}_price", @id
            result = "#{name.to_s.capitalize}: #{info} ($#{price})"
            return "* #{result}" if price > 100
            result
        end
    end  
end

2.2 method_missing 幽灵方法(Ghost Method)

2.2.1 method_missing(symbol [, *args] ) 方法

Invoked by Ruby when obj is sent a message it cannot handle. symbol is the symbol for the method called, and args are any arguments that were passed to it. By default, the interpreter raises an error when this method is called. However, it is possible to override the method to provide more dynamic behavior. If it is decided that a particular method should not be handled, then super should be called, so that ancestors can pick up the missing method.

2.2.2 使用 method_missing 重构 computer.rb
class Computer
    def initialzie(id, data_source)
        @id = id
        @data_source = data_source
    end
    def method_missing(name, *args)
        super unless @data_source.respond_to? "get_#{name}_info"
        info = @data_source.send "get_#{name}_info", @id
        price = @data_source.send "get_#{name}_price", @id
        result "#{name.to_s.capitalzie}: #{info} ($#{price})"
        return "* #{result}" if price > 100
        result       
    end
end
2.2.3 覆盖 respond_to?
class Computer
    def initialzie(id, data_source)
        @id = id
        @data_source = data_source
    end
    def method_missing(name, *args)
        super unless @data_source.respond_to? "get_#{name}_info"
        info = @data_source.send "get_#{name}_info", @id
        price = @data_source.send "get_#{name}_price", @id
        result "#{name.to_s.capitalzie}: #{info} ($#{price})"
        return "* #{result}" if price > 100
        result       
    end
    def respond_to?(name)
        @data_source.respond_to?("get_#{name}_info") || super
    end
end
2.2.4 白板类(BlankSlate)

有时候所要代理的方法,已经存在于类或者祖先类种,这种情况下,调用那个方法时,就不会经过 method_missing 代理,因此这时,需要将从父类中继承来的那些不需要的方法都删除掉:

class Computer
    instance_methods.each do |method|
        undef_method method unless method.to_s =~ /method_missing|respond_to\?/
    end
      
end

用于从一个类中删除实例方法的方法:

  • Module#undef_method(symbol)

Prevents the current class from responding to calls to the named method. Contrast this with remove_method, which deletes the method from the particular class; Ruby will still search superclasses and mixed-in modules for a possible receiver. String arguments are converted to symbols.

  • Module#remove_method(symbol)

Removes the method identified by symbol from the current class. For an example, see Module.undef_method. String arguments are converted to symbols.

BasicObject

从 Ruby 1.9 开始,白板技术被集成到语言自身中,在过去的版本中,Object是类体系结构的根节点。在 Ruby 1.9 中,Object类有一个名叫 BasicObject 的超类,它只提供几个很基本的方法:

p BasicObject.instance_methods

=> [:==, :requals?, :!, :!=, :instance_eval, :instance_exec, :__sned__]

默认情况下,类还是会从 Object 继承,从 BasicObject 继承来的类会自动成为白板类。

3. 代码块

3.1 当前块

在一个方法中,可以向Ruby询问当前的方法调用是否包含块。这可以通过 Kernel#block_given?() 方法来做到:

def a_method
    return yield if block_given?
    'no block'
end

a_method
a_method { "here's a block!" }

3.2 使用 Ruby 实现 C# 的 using 关键字

设想在写一个 C# 程序,这个程序会连接一个远程服务器,并有一个对象表示这个连接:

RemoteConnection conn = new RemoteConnection("my_server");
String stuff = conn.readStuff();
conn.dispose();

这段代码在使用了连接后,会正确的释放连接。然而,它并没有处理异常。如果 readStuff() 方法抛出一个异常,那么 conn 对象将永远不会得到释放。代码需要将异常管理起来,以便不管是否发生异常都能正确地释放连接。幸运的是,C# 提供了一个叫做 using 的关键字,它能帮助你处理整个过程:

RemoteConnection conn = new RemoteConnection("some_remote_server");
using (conn)
{
    conn.readSomeData();
    doSomeMoreStuff();
}

这个 using 关键字期望 conn 对象有一个名为 dispose() 的方法,当大括号中的代码执行完后,不管有没有异常抛出,这个方法都会被自动调用。

OK,下面这个是用Ruby实现的 using 关键字:

module Kernel
    def using(obj)
        begin
            yield
        rescue
            obj.dispose
        end  
    end
end

3.3 闭包

正如上图所示,块不仅仅是一段浮动的代码。你不可能在真空中运行代码。当代码运行时,它需要一个执行环境:局部变量、实例变量、self …… 既然这些东西是绑定在对象上的名字,就可以把它们简称为绑定(binding)。块的要点在于它们是完整的,可以立即运行。它们既包含代码,也包含一组绑定。

那么块是从哪里获得它的绑定的呢?当定义一个块时,它会获取当时环境中的绑定,并且把它传给一个方法时,它会带着这些绑定一起进入该方法:

def my_method
    x = "Goodbye"
    yield "cruel"
end

x = "Hello"
my_method {|y| "#{x}, #{y} world" }  # => "Hello, cruel world"

当创建块时会获取到局部绑定(比如上面的x),然后把块连同它自己的绑定传给一个方法。在上面的例子中,块的绑定中包括一个名为 x 的变量。虽然在方法中定义了一个变量 x,块看到的 x 也是在块定义时绑定的 x,但是方法中的 x 对这个块来说是不可见的。基于这样的特性,计算机科学家喜欢把块称为 闭包(Closure)

3.3.1 块局部作用域

块会在定义时获取周围的绑定。你可以在块的背部定义额外的绑定,但是这些绑定在快结束时就会消失:

def my_method
    yield
end

top_level_variable = 1
my_method do
    top_level_variable += 1
    local_to_block = 1
end

top_level_variable  # => 2

local_to_block      # => Error!

警告: 在 Ruby 1.8及之前的版本中,块参数对粗心者而言有一个陷阱。跟你期望的相反,块会覆盖具有相同名字的局部变量:

def my_method
    yield(2)
end

x = 1
my_method do |x|
    # 不错什么特殊的操作

end
x  # => 2

当把块参数命名为 x 时,块发现当前上下文中已存在一个 x 变量,于是它把这个值传给了块。这种让人吃惊的行为在过去常常是引发 bug 的根源。有条好消息是,在 Ruby 1.9 中这个问题已经被修正了。

3.4 作用域门

Ruby 程序会在三个地方关闭前一个作用域,同时打开一个新的作用域:

  • 类定义
  • 模块定义
  • 方法定义

只要程序进入类或魔窟开及方法的定义,就会发生作用域切换。这三个边界分别用 class、moduledef关键字作为标志。每一个关键字都充当了一个作用域门(Scope Gate)

示例:

v1 = 1
class MyClass        # => 作用域门:进入 class

    v2 = 2
    local_variables     # => ["v2"]

    
    def my_method    # => 作用域门:进入 def

        v3 = 3
        local_variables
    end                  # => 作用域门:离开 def

    
    local_variables     # => ["v2"]

end                      # => 作用域门:离开 class

obj = MyClass.new
obj.my_method         # => [:v3]

obj.my_method         # => [:v3]

local_variables         # => [:v1, :obj]

Kernel#local_variables

Returns the names of the current local variables.

3.5 全局变量和顶级实例变量

全局变量可以在任何作用域中访问:

def a_scope
    $var = "some value"
end

def another_scope
    $var
end

a_scope
another_scope    # => "some value"

全局变量的问题在于系统的任何部分都可以修改它们。因此,你会立即发现几乎没法追踪谁把它们改成了什么。正因为如此,基本的原则是:如非必要,尽可能少使用全局变量:

@var = "The top-level @var"

def my_method
    @var
end

my_method  # => "The top-level @var"

如上面的代码所示,只要 main 对象在扮演 self 的角色,就可以访问一个顶级实例变量。但当其他对象成为 self 时,顶级实例变量就退出作用域了:

class MyClass
    def my_method
        @var = "This is not the top-level @var!"
    end
end

由于不像全局变量那么有全局性,一般认为顶级示例变量比全局变量更安全。

3.6 扁平化作用域

  • 当想要使变量穿越过类定义时,使用 Class.new

  • 当想要使变量穿越过方法定义时,使用 Module#define_method

示例:

my_var = "Success"    
class MyClass
    # 希望在这里打印 my_var

    
    def my_method
        # 还有这里 ……

    end  
end

----------->

my_var = "Success"
MyClass = Class.new do
    p my_var
    define_method :my_method do
        p my_var
    end
end

3.7 上下文探针 Object#instant_eval()

  • instance_eval(string [, filename [, lineno]] )
  • instance_eval {| | block }

Evaluates a string containing Ruby source code, or the given block, within the context of the receiver (obj). In order to set the context, the variable self is set to obj while the code is executing, giving the code access to obj’s instance variables. In the version of instance_eval that takes a String, the optional second and third parameters supply a filename and starting line number that are used when reporting compilation errors.

示例:

class MyClass
    def initialize
        @v = 1
    end
end

obj = MyClass.new
obj.instance_eval do
    self            # => #<MyClass:0x3340dc @v=1>

    @v              # => 1

    @v += 1
    @v              # => 2

end

Ruby 1.9 中引入了一个名为 instance_exec() 的方法,它跟 instance_eval() 的功能相似,但它允许对块传入参数:

class C
    def initialize
        @x, @y = 1, 2
    end
end

C.new.instance_exec(3) {|arg| (@x + @y) * arg}       # => 9

3.8 可调用对象

可调用对象分为三种:

  • proc
  • lambda
  • 方法
3.8.1 proc 和 lambda 的区别

使用 Kernel#lambda() 生成的 Proc 对象就是 lambda,通过其他方式生成的 Proc 对象都简称为 proc。 lambda 和 proc 主要有两个方面的区别:

  • 在 lambda 中调用 return 是从该 Proc 对象中返回,而不是从调用的方法中返回。proc 中调用 return 是从该 Proc 对象中返回
  • lambda 调用中对参数的个数会有严格的检查,而 proc 没有。

综上所述,lambda 的行为更像是普通的方法/函数,因此一般都使用 lambda 对象,除非是想使用 proc 的某种特殊功能。

另外,在 Ruby 1.8 中 Kernel#proc()Kernel#lambda() 的别名,而在 Ruby 1.9 中 Kernel#proc()Proc.new() 的别名。

3.8.2 方法

可以通过 Object#method() 获取一个 Method 对象:

class MyClass
    def initialize(value)
        @x = value
    end
    
    def my_method
        @x
    end  
end

obj = MyClass.new(1)
m = obj.method :my_method
m.call()                       # => 1

可以用 Method#unbind() 把一个方法跟它所绑定的对象相分离,该方法再返回一个 UnboundMethod 对象。你不能执行 UnboundMethod 对象,但能把它绑定到一个对象上,使之再次成为一个 Method 对象:

unbound = m.unbind
another_obj = MyClass.new(2) 
m = unbound.bind(another_obj)
m.call()                           # => 2

3.9 RedFlag

初版:

def event(msg)
    puts msg if yield
end

Dir.glob("*events.rb").each {|file| load file}

第2版:

def event(name, &block)
    @events[name] = block
end

def setup(&block)
    @setups << block
end

Dir.glob("*events.rb").each do |file|

    @events = {}
    @setups = []
    load file
    @events.each_pair do |k, v|
        env = Object.new
        @setups.each do |su|
            env.instance_eval &su
        end
        puts k if env.instance_eval &v
    end
end

第3版:

->{

    events = {}
    setups = []

    Kernel.send :define_method, :event do |name, &block|
        events[name] = block
    end

    Kernel.send :define_method, :setup do |&block|
        setups << block
    end

    Kernel.send :define_method, :each_event do |&block|
        events.each_pair do |k, v|
            block.call(k, v)
        end
    end

    Kernel.send :define_method, :each_setup do |&block|
        setups.each do |su|
            block.call(su)
        end
    end

}.call

Dir.glob("*events.rb").each do |file|
    load file
    each_event do |name, event|
        env = Object.new
        each_setup do |su|
            env.instance_eval &su
        end
        puts name if env.instance_eval &event
    end
end

4. 类定义

4.1 Module#class_eval() 方法

class_eval 用于在一个类中定义实例方法:

class Hello; end

Hello.class_eval do
    def hello
        puts "hello world"
    end
end

h = Hello.new
h.hello       # => hello world

instance_eval 用于在一个类中定义类方法:

class Hello; end

Hello.instance_eval do
    def hello
        puts "hello world"
    end
end

Hello.hello      # => hello world

4.2 类实例变量和类变量

4.2.1 类实例变量

Ruby 解释器假定所有的实例变量都属于当前对象 self。在类定义时也是如此:

class MyClass
    @my_var = 1
end

在类定义的时候,self 的角色由类本身担任,因此实例变量 @my_var 属于这个类。类的实例变量不同于类的对象的实例变量。另外一个例子:

class MyClass
    @my_var = 1
    def self.read; @my_var; end
    def write; @my_var = 2; end
    def read; @my_var; end
end

obj = MyClass.new
obj.write
obj.read          # => 2

MyClass.read     # => 1
4.2.2 类变量

类变量与类示例变量不同,它们可以被自雷或类的实例所使用(在这个意义上,它们更像是 Java 的静态成员)。

class D < C
    @@v = 1
    def my_method; @@v; end
end

D.new.my_method           # => 1

不幸的是,类变量有一个很不好的怪癖。下面是一个例子:

@@v = 1

class MyClass
    @@v = 2
end

@@v         # => 2

的搭配这样的结果是因为类变量并不真正属于类————它们属于类体系结构。由于 @@v 定义于 main 的上下文,它属于 main 的类 Object,所以也属于 Object 的所有后台。 MyClass 继承自 Object,因此它也共享了这个类变量。

从技术上讲,尽管这种行为可以理解,但它还是很容易把你绊倒,因为可能会遇到上面所示的意外事件,现在绝大多数 Ruby 主义者都避免使用类变量,而尽量使用类实例变量。

4.3 Class.new

Class.new(super_class=Object) { |mod| … }

Creates a new anonymous (unnamed) class with the given superclass (or Object if no parameter is given). You can give a class a name by assigning the class object to a constant.
If a block is given, it is passed the class object, and the block is evaluated in the context of this class using class_eval.

示例:

Employee = Class.new(Person) do
    def hello
        ...
    end
end

e = Employee.new
e.hello

4.4 类方法的写法

1.

def MyClass.my_method; end

2.

class MyClass
    def self.my_method; end
end

3.

class MyClass
    class << self
        def my_method; end
    end
end

在日常编程中应该使用哪种语法,这在很大程度上取决于个人喜好。大多数人认为 self 形式的语法可读性更高,而一些人则明确指出 eigenclass 才是方法的真正所用之处。专家级的 Ruby 程序猿都会不屑于使用“类名”方法的语法,因为这种方式重复了类的名字,给重构带来了不便。

4.5 类方法和Module#include() 以及 Object#extend()

使用 Module#inculde() 方法可以混入一个模块,使得在该模块中定义的方法变为调用类的示例方法:

module Hello
    def hello
        "hello world"
    end
end

class Person
    include Hello
end

p = Person.new
p.hello          # => "hello world"  

结合 eigenclassModule#include() 可以将模块中定义的方法变为调用类的类方法:

module Hello
    def hello
        "hello world"
    end
end

class Person
    class << self
        include Hello
    end
end

Person.hello         # => "hello world"

为了简化上面的操作,Ruby 提供了 Object#extend 方法,例如:

module Hello
    def hello
        "hello world"
    end
end

class Person; end
Person.extend Hello
Person.hello         # => "hello world"


p = Person.new
p.extend Hello
p.hello                  # => "hello world"

4.6 环绕别名

编写环绕别名的步骤:

  1. 给方法定义一个别名
  2. 重新定义这个方法
  3. 在新的方法中调用老的方法

示例:

class String
    alias old_length length
    def length
        if old_length > 5
            "long"
        else
            "short"
        end
    end
end  

警告: 永远不要把一个环绕别名加载两次!

5. 编写代码的代码

5.1 Binding 对象

Binding 就是一个用对象表示的完整作用域。你可以通过创建 Binding 对象来捕获并带走当前的作用域。接下来,你还可以通过 eval() 方法、instance_eval() 方法或 class_eval() 方法,在 Binding 对象所携带的作用域中执行代码。

可以使用 Kernel#binding() 方法来创建 Binding 对象:

class MyClass
    def my_method
        @x = 1
        binding
    end
end

b = MyClass.new.my_method

*eval() 方法家族,可以给它们传递一个 Binding 对象作为额外的参数,代码就可以在这个 Binding 对象所携带的作用域中执行:

eval "@x", b          # => 1

Ruby 还提供了一个名为 TOPLEVEL_BINDING 的预定义常量,它表示顶级作用域的 Binding 对象。你可以在程序的任何地方访问这个顶级作用域:

class AnotherClass
    def my_method
        eval "self", TOPLEVEL_BINDING
    end
end

AnotherClass.new.my_method            # => main

从某种意义上说,你可以把 Binding 对象看成是一个比块更“纯净”的闭包,因为它们只包含作用域而不包含代码。

5.2 代码字符串

代码字符串可以像块一样访问局部变量:

array = ['a', 'b', 'c']
x = 'd'
array.instance_eval "self[0] = x"

array        # => ['d', 'b', 'c']

5.3 Kernel#eval 与 Ruby 安全级别

使用 Kernel#eval() 的问题在于,这存在遭受代码注入攻击的危险。

Ruby 会自动把不安全的对象——尤其是从外部传入的对象——标记为被污染的。污染对象包括程序从 Web 表单、文件和命令行读入的字符串,甚至包括系统变量。每次从污染字符串运算而来的新字符串,也是被污染的。下面的例子通过调用 tainted?() 方法来判断类是不是被污染了:

user_input = "User input: #{gets()}"
puts user_input.tainted?

每次都要检查字符串是否被污染很麻烦,Ruby 提供了一种叫做安全级别的概念,它能很好地弥补污染对象的不足,当设置一个安全你级别(可以通过给 $SAFE 全局变量赋值来实现)时,就禁止了某些特定的潜在危险操作。

有五个安全级别可供选择,从默认的 0(这里是一个“嬉皮士公社”,在这儿你可以不受约束,也可以格式化硬盘)到 4(这里是“军事管辖区”,在这儿你甚至不能自由地退出程序)。例如,在安全级别 2 上,会禁止绝大多数文件相关工作。值得注意的是,在任何大于 0 的安全级别上,Ruby 都会拒绝执行污染的字符串:

$SAFE = 1
user_input = "User input: #{gets()}"
eval user_input

为了调节安全性,可以在执行代码字符串之前显式去除它的污染小(通过调用 Object#untaint() 方法),然后可以依赖安全级别来禁止注入文件操作这样的危险动作。

5.4 类扩展混入(Class Extension Mixins)

编写类混入扩展的步骤:

  1. 定义一个模块,姑且叫做 MyMixin
  2. MyMixin 中定义一个内部模块(通常把它叫做 ClassMethods),并给它定义一些方法。这些方法最终会成为类方法。
  3. 覆盖 MyMixin#included() 方法来用 ClassMethods 扩展包含者(使用 extend() 方法)。

示例:

module MyMixin
    module ClassMethods
        def hello
            "hello world"
        end
    end
    
    def self.included(base)
        base.extend ClassMethods
    end
end

class Person
    include MyMixin
end

Person.hello         # => "hello world"

5.5 CheckedAttributes

module CheckedAttributes
    module ClassMethods
        def attr_checked name, &block
            define_method name do
                instance_variable_get "@#{name}"
            end
            
            define_method "#{name}=", value do
                raise "Invalid Attribute Value" unless block.call(value)
                instance_variable_set "@#{name}", value
            end
        end
    end
    def self.included(base)
        base.extend ClassMethods
    end
end

class Person
    attr_checked :age do |age|
        age > 18
    end
end

p = Person.new
p.name = 20
p.name               # => 20

p.name = 10           # => raise "Invalid Attribute Value"
← 一些杂片 Ruby 的 Time 类和 DateTime 类的区别 →
 
comments powered by Disqus