Ruby 元编程:第二部分

来源:开源中国社区 作者:oschina
  

欢迎回到Ruby 元编程系列文章。如果你没有理解透彻上一篇文章,你可能需要回顾一下

 Ruby元编程: 第一部分,才能更好理解本文讨论的内容。在上一篇文章我们讨论了 Ruby对象模型、祖先链,动态定义方法,动态调用方法,以及幽灵方法。接下来我们将在本文讨论剩下的元编程理论,并向你展示一些强大实用的工具。

闭包

我们通过作用域寻址来讨论一下闭包。在Ruby中有三个作用域分界线(可以形象地称之为作用域门):

  • 类定义

  • 模块定义

  • 方法

以下代码看起来是不太可能实现的:

1
2
3
4
5
6
7
8
my_var = "Success"
class MyClass
    # We want to print my_var here...
  
    def my_method
        # ..and here
    end
end

但是通过元编程,我们可能穿越作用域门,使之成为可能。在这之前,我们先讨论一下Ruby中类定义的两种方法。

静态定义类

这是我们通常使用的方法:

1
2
3
4
5
6
7
8
9
10
# the normal way
class Book
    def title
        "All My Friends Are Dead"
    end
end
  
puts Book.new.title
  
# => All My Friends Are Dead

这个方法没有什么新奇的,但是Ruby还可以在运行时定义类,下面举例说明。

动态定义类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Book = Class.new do
    def foo
        "foo!"
    end
  
    #Both method declaration types work
  
    define_method('title'do
        "All My Friends Are Dead"
    end
end
  
puts Book.new.foo
puts Book.new.title
  
# => foo!
# => All My Friends Are Dead

这是Ruby的另一种定义类的方式。如果你记得我们 上一篇文章 所讨论的Ruby对象模型,你就会知道 Ruby的 class 也是一个对象,它的类型就是 Class。这似乎有点困惑,但仔细琢磨这句话,意味着我们可以根据类 Class 来实例化一个对象,然后这个对象也是一个类。刚才的例子我们正是这么做的,我们在运行时调用 Class.new 生成了一个类。

这么做使得 my_var 变量可以穿越作用域门进入到类定义块中。做这段代码中,你可以有两种方式定义方法。你可以用传统的方式定义类,就像我们定义 foo方法一样--但是这种方式仍然存在作用域门,方法内无法直接引用 my_var 变量。如果你想要 my_var 变量可以穿越作用域门,你需要使用动态定义方法-就像代码中定义 title 方法一样。

以下是我们所讨论的作用域的完整例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
my_var = "Success"
  
MyClass = Class.new do
    "#{my_var} in the class definition"
  
    # Have to use dynamic method creation to access my_var
    define_method :my_method do
        "#{my_var} in the method"
    end
end
  
puts MyClass.new.my_method
  
# => Success in the method

这种看起来“没有作用域”的程序,我们称之为 Flat Scope (扁平作用域) 。是否使用这种方式由你选择,Ruby 提供这种选项供你选择。

Blocks, Procs, and Lambdas

当开发者的方案中不是必须使用元编程的时候,他们通常不会完全理解 blocks, procs, 以及lambdas 表达式,以此为想给大家解惑一下blocks, procs, 以及lambdas 表达式的原理。首先,大多数Ruby的元素都是对象,但 Blocks 不是。当需要传递 Blocks时,你需要使用 & 符号。

1
2
3
4
5
6
7
8
def my_method(greeting)
    "#{greeting}, #{yield}!"
end
  
my_proc = proc { "Bill" }
puts my_method("Hello", &my_proc)
  
# => Hello, Bill

我在一个作用域中定义了一个 block 然后使用 & 符号传递它到一个方法中,方法再使用 yield 来调用这个 block。接下来看看 procs 和 lambdas有什么不同。

主要有两点区别:

  • 如果参数不匹配 Lambdas 会抛出一个 ArgumentError  异常。Procs 不会。

  • Lambdas中的 return 将在它定义的作用域中发生,而 Procs 的 return 会再它被调用的那个作用域中发生。

第一个区别我们很容易理解,第二个我们需要举例说明一下。

请看 lambda 的例子:

1
2
3
4
5
6
7
8
9
def lambda_example
  l      = lambda {|x,y| return x * y }
  result = l.call(24) * 10
  return result
end
  
puts lambda_example
  
# => 80

这段代码的执行结果和预期一样。在 lambda_example 这个方法中,我们定义了一个 lambda 块,这个 lambda 仅接受两个参数,然后返回这两个参数的乘积。然后我们调用这个lambda块,并传入2,4两个参数,结果再乘以10. 最后代码返回80.

如果我们调用这个 lambda的时候传入多于或少于两个参数,那将会引发 ArgumentError 异常。

我们再来看看 procs 有什么不一样的地方:

1
2
3
4
5
6
7
8
9
def proc_example
    p      = proc {|x,y| return x*y }
  result = p.call(24) * 10
  return result
end
  
puts proc_example
  
# => 8

等等-这段代码和前一段代码唯一的差别只是我们将 lambda 换成了 proc -结果居然变成了 8?

是的,结果就是8,原因是 procs return 操作是在调用它的作用域中生效的。尽管你在proc的定义块中return,return的语法并没有在定义块中生效,而是你调用这个proc的作用域,以上例子是在第三行。因此,无论何时我们执行这段代码它都是返回8,而不再继续执行下面代码。也就是说以上代码第四行将永远不被执行,因为在proc 块中以及return了。

lambda 和 procs 的另一个不明显的区别是proc 接受到不同于你期望的参数个数也不会引发 ArgumentError 异常。

总结一下闭包,作用域通常如预期的工作,一旦你知道如何操控它,它将是一个很强大的工具。但请明确你知道自己在干什么。接下来我们看看 evals。


Evals

在Ruby中,我们有三种 evals:

  1. Instance Eval

  2. Class Eval

  3. Eval

Instance Eval

instance_eval 是一个可以破坏对象封装、可以直接操作对象内部元素的方法。我们来看看例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Book
  def initialize
    @v 1  # => Private variable
  end
end
  
obj = Book.new
  
x = 2
obj.instance_eval { @v = x }
puts obj.instance_eval {@v}
  
# => 2

在这个 obj 对象中v是一个私有实例变量。如果我们直接调用 obj.v 将引发异常-但如果我们使用 instance_eval,不仅可以访问和修改私有实例变量,还可以在块中调用块之外的变量,因为 instance_eval的块并不是一个作用域门。以上例子就是一个很好的举证,我们可以看到instance_eval的强大之处。但是要小心,这个方式你可以访问对象内的任何一个属性(包括私有的),然而实例方法和实例变量被设置为私有通常是有原因的。

Class Eval

尽管我们有了类打开以及动态类定义,我们依然无法用一个闭包(像一个方法)去更新一个类的定义。我们也无法根据一个变量来进入一个指定的类,只能用常量。但是 class_eval 可以满足这些需求。

1
2
3
4
5
6
7
8
9
10
11
def add_method_to(a_class)
  
    a_class.class_eval do
        def m; 'Hello!'end
    end
end
  
add_method_to(String)
puts "foo".m
  
# => Hello!

第一,class_eval 让我们可以在任何时候进入一个已经存在的类,就像以上代码中可以在一个方法的定义块中进入一个已经存在的类。第二,class_eval 允许我们根据一个变量而不是一个常量来进入指定的类,这点很重要。在这个例子中,我们将 String 这个静态变量传入add_method_to 方法中,负值给方法的a_class 变量。然后就可以根据变量 a_class 的值‘String’,从而进入 String类中。我们无法根据一个变量来使用类打开的方式,比如这样的代码:class a_string ,这只会尝试去打开名为 a_string 的这个类,而不是把a_string这个变量的值当作一个类名。

我们也可以使用 class_eval 来绕过作用域门,从而使用扁平作用域,类似于我们之前讨论的动态类定义。

Eval

现在我们讨论一下最后一个eval 方法,就是 eval 。这个方法及其容易理解,但也是非常强大也非常危险的。eval方法接受一个字符串参数,然后把这个字符串作为Ruby代码在调用的地方直接执行。以下简单的例子,使用eval方法忘一个数组增加一个元素:

1
2
3
4
5
6
7
array   = [1020]
element = 30
  
eval("array << element")
puts array
  
# => [10, 20, 30]

在这段代码中我们并没有写Ruby代码往数组增加一个元素,只是写了一个字符串,让eval去执行这个操作。这个例子中并没有体现出 eval的价值,我们再来看一个深入一点的例子来看看eval的强大之处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
klass = "Book"
instance_var = "title"
  
eval <<-CODE # This is just a multi-line string
    class #{klass}
        attr_accessor :#{instance_var}
  
        def initialize(x)
            self.#{instance_var} = x
        end
    end
CODE
  
b = Book.new("Moby Dick")
puts b.title
  
# => Moby Dick

如果你不熟悉代码中eval后面的语法,其实那只是一个Ruby的多行字符串而已multiline string。在这个例子中,我们有两个局部变量,与我们调用eval在同一个作用域,然后我们用这两个变量打开了一个类,新增了一个 attr_accessor 并写了一个构造函数。我们是将这两个局部变量植入到多行字符串中的。这个多行字符串被当成Ruby代码有效地执行了,如果不使用eval,这是永远无法做到的。明白了eval的强大了吗?

 

本文转自:开源中国社区 [http://www.oschina.net]
本文标题:Gallery 3.0.9 发布,Web 相册管理系统
本文地址:
http://www.oschina.net/translate/metaprogramming-in-ruby-part-2
参与翻译:
Yashin

英文原文:Metaprogramming in Ruby: Part 2


时间:2016-03-24 08:10 来源:开源中国社区 作者:oschina 原文链接

好文,顶一下
(0)
0%
文章真差,踩一下
(0)
0%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量