Ruby元编程-学习笔记(三)-代码块

  • 块可以定义在大括号中, 也可以放在do…end关键字中, 一般来说,只有一行的块使用{},而对多行的块使用do…end
  • 只有在调用一个方法时才可以定义一个块,块会被直接传递给这个方法,然后该方法可以通过yield关键字回调这个块.
  • 块可以有自己的参数,当回调块时,可以像调用方法一样为块提供参数,并且块中最后一行代码执行的结果会被作为返回值.
  • 通过Kernel#block_given?()方法可以询问当前方法调用是否包含块
def my_method(a, b)
    return a + yield(a, b) if block_given?
    'no block'
end

my_method(1, 2)                         # => "no block"
my_method(1, 2) {|x, y| (x + y) * 3}    # => 10

对于在块中出现异常的情况,可以使用ensure关键字保证在出错的情况下,还能执行代码块,该方法常用于释放连接的情况,即使块出错也会保证连接释放.使用ruby模仿C#的关键字using.

def using(connect)
    begin
        yield(connect)              # 执行块中内容
    ensure
        puts "dispose connection"   # 确保该段代码执行 
    end
end
using(1) {|connect| connect / 0}    # => "dispose connection"
                                    # => 报告异常

闭包

代码运行时需要一个执行环境:局部变量,实例变量,self等, 这些绑定在对象上的名字我们简称为绑定.
块包含代码和一组绑定,当创建块时会获得到局部绑定,然后连同自己的绑定传递给方法,而方法中的变量对于块是不可见的.

def my_method
    var = "my_method()"
    yield("block")
end
my_method { |text| "#{var}, #{text}" }  # => 报错,没有var
var = "Hello"
my_method { |text| "#{var}, #{text}" }  # => Hello, block

在上述例子中,第一次块的绑定中无var变量, 导致出错,第二次块绑定中包含var变量,虽然my_method中包含了var但对于块不可见, 这种特性称之为闭包.

作用域

在C++和Java等语言中,有内部作用域和外部作用域的概念,内部作用域可以看到外部作用域的变量, 但在Ruby中,没有这种嵌套式的作用域,它们的作用域之间是分开的: 一但进入一个新的作用域,原先的绑定就会被替换为一组新的绑定.

v1 = 1
class MyClass
    v2 = 2
    local_variables      # => [:v2]
    def my_method
        v3 = 3
        local_variables
    end
    local_variables      # => [:v2]
end
obj = MyClass.new
obj.my_method            # => [:v3]
local_variables          # => [:v1, :obj]

从上述代码可以看出在程序切换作用域时,绑定会发生变化.

作用域门

程序会在三个地方关闭前一个作用域,同时打开新的作用域:
- 类定义
- 模块定义
- 方法
这三个边界分别用class, module和def关键字作为区分标识, 每个关键字都充当了一个作用域门.

全局变量和顶级实例变量

全局变量在任何作用域中都可以访问,正因如此,为了安全尽量少使用全局变量.
有时可以使用顶级实例变量来代替全局变量, 它们是顶级对象main的实例变量.

@var = "The top-level @var"

def my_method
    @var
end

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

如上述代码所示,只要main对象扮演self的角色,就可以访问到顶级实例变量, 但当其它对象成为self时,顶级实例变量就退出作用域了,一般情况下,顶级实例变量比全局变量更安全.

扁平化作用域

既然了解了作用域的功能,那么如何让绑定穿过作用域门就成了一大问题.
Ruby中的类是Class类的实例,所以使用Class.new可以避免class关键字的出现,这样可以让变量穿过该作用域,同样对于def关键字,我们可以使用define_method()方法.

my_var = "Success"

MyClass = Class.new do
    puts "#{my_var} in the class definition!"
    define_method :my_method do
        puts "#{my_var} in the method!"
    end
end

MyClass.new.my_method
=>  Success in the class definition!
    Success in the method!

这样可以让一个作用域看到另一个作用域, 我么称这种技巧为嵌套文法作用域,也称之为扁平作用域.

共享作用域

但是对于想让某些变量在特定的方法中共享,而不希望其它方法访问,这样便需要另一种技巧共享作用域.

def define_methods
    shared = 0
    Kernel.send :define_method, :counter do
        shared
    end
    Kernel.send :define_method, :inc do |x|
        shared += x
    end
end

define_methods
counter         # => 0
inc(4)
counter         # => 4

上述代码中,通过send给Kernel模块定义counter方法和inc方法, 这样只有Kernel的counter和inc方法可以访问shared变量,而其它方法则无法访问,因为这个变量被def作用域门保护这, 这种用于共享变量的技巧称为共享作用域.

instance_eval()

class MyClass
    def initialize
        @v = 1
    end
end

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

使用instance_eval方法时,该块的接受者会成为self, 因此他可以访问接收者的私有方法和实例变量, instance_eval()方法可以在不碰其它绑定的情况下修改self对象.

v = 2
obj.instance_eval {@v = v}
obj.instance_eval {@v}      # => 2

这三行代码在同一扁平作用域中执行,故都能访问局部变量v, 而且由于块把运行它的对象作为self,所以它们也能访问obj的实例变量@v.
在这些情况中,可以把传递给instance_eval()方法的块称为一个上下文探针.

洁净室

仅仅为了在其中执行块而创建的对象称之为洁净室

class CleanRoom
    def complex_calc
        # ...
    end

    def do_something
        # ...
    end
end

clean_room = CleanRoom.new
clean_room.instance_eval do
    if complex_calc > 10
        do_something
    end
end
# 洁净室仅仅是一个用来执行块的环境,它通常会暴漏若干有用的方法提供调用.

可调用对象

从实质上看,使用块有两个步骤:1)将代码打包备用;2)调用块来执行代码.在ruby中,有常用三种情况来打包代码.
- 使用proc, proc实质上是由一个块转换成的对象.
- 使用lambda, 它与proc类似
- 使用方法’

Proc对象

Ruby在标准库中提供了Proc类,可以通过Proc.new方法创建一个Proc,之后可以使用Proc#call()方法进行调用,称之为延迟执行.
Ruby还提供了两个内核方法,用于转换块,lambda()和proc().

inc = Proc.new {|x| x + 1}
inc.call(2)                 # => 3

dec = lambda {|x| x - 1}
dec.call(2)                 # => 1

块就类似与方法的额外参数名,大多情况下,方法可以通过yield语句直接运行一个块,但是对与把块传递给另一个方法或者把该块转换为Proc,yield将力不从心. 这时,我们可以使用&操作符,&块名即可.
注:要将块附加到一个绑定上,可以给这个方法添加一个特殊参数,该参数必须是参数列表最后一个,如&block, block为块名.

def math(a, b)
    yield(a, b)
end

def teach_math(a, b, &operation)
    puts "Let's do the math:"
    puts math(a, b, &operation)
end

teach_math(1, 2) {|x, y| x + y}

=> Let's do the math:
   3

&操作符,就像一个转换, 它可以将块转换为Proc对象,相反也可以将Proc对象转换为一个块.

def my_method(&the_proc)    # 通过&操作符,将块转换为Proc对象
    the_proc
end
p = my_method {|name| "Hello, #{name}!"}
p.class                 # => Proc
p.call("Bill")          # => Hello, Bill!
def my_method(greeting)
    puts "#{greeting}, #{yield}!"
end

my_proc = proc { "Bill" }
my_method("Hello", &my_proc)    # 通过&操作符将proc对象转换为块

=> Hello, Bill!

proc与lambda对比

  • proc与lambda的return不同
    • 在lambda中,return表示仅从该lambda中返回
    • 在proc中,return表示从定义proc的作用域中返回

def my_method(name)
    name.call       # 调用Proc对象
end

l = lambda {return "lambda"}
my_method(l)        # => lambda

p = proc {return "proc"}
my_method(p)        # => 出错:LocalJumpError
p.call              # => 出错:LocalJumpError
由于proc的返回是从它定义的作用域中返回,而上述代码定义proc对象的作用域为顶级作用域main, 故而返回错误.
def my_method
    p = proc { return "proc" }
    p.call
    return "Finial my_method"
end

my_method           # => "proc"

# 由于proc会在定义的作用域中返回,所以并没有执行my_method最后一行.
  • proc和lambda检查参数的方式不同
  • proc中,对于多余的参数,proc则会忽略掉
  • lambda中, 对于多余的参数,则会失败,同时抛出ArgumentError

方法

通过调用Objec#method()方法可以获得一个用Method对象表示的方法,之后可以使用Method#call()对其进行调用.

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

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

Method方法类似lambda, 但有一个重要的区别:lambda在定义它的作用域中执行,而Method对象在它自身所在的对象的作用域中执行.
Method#unbind()方法可以把一个方法跟它所绑定的对象分离,该方法再返回一个UnboundMethod对象,虽然无法直接执行UnboundMethod对象,但可以把它绑定到一个对象上,使其再次成为一个Method对象.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值