「Ruby」 Block的理解和使用


Block是Ruby中比较重要的概念,也算是一块比较难啃的骨头。笔者在学Ruby基础的时候,碰到了很多关于块的“神奇操作“,但是在google上搜了很久也没有找到满意的答案(毕竟Ruby相对小众一点)。本想先跳过,在学完rails之后再补习ruby基础,没成想打开rails官方教程后,密密麻麻的全是块的操作...(晕)。于是不得不回来继续啃,特以此篇博客作为记录。

Proc和Lambda

在理解Block之前,我们必须要清楚另一个概念——Proc。Proc实际上就是“一小段代码”,它在定义之后可以在其他地方被调用,在调用时也可以传入一些变量,这跟函数是很相似的。不过与函数不同的是,Proc对象必须通过call方法才能被调用。

# 创建方式
# 创建一般Proc
p = Proc.new{|x| x + 1}
p = proc {|x| x + 1}
# 创建lambda类型的Proc
p = ->(x) {x + 1}
p = lambda {|x| x + 1} 

# 调用方式
p.call(1)

lambda是一种特殊的Proc,它和普通Proc有以下两点区别——
- lambda在调用时会检查参数的个数。如果传入的参数太多或太少,都会抛出ArgumentError异常。而普通Proc不会对参数个数进行检查。
- lambda可以使用return返回某个对象,其行为和函数很相似。但是普通Proc不可以使用return,否则会抛出LocalJumpError异常。

Q: 如何判断一个Proc对象是lambda还是普通Proc?
A: 只需要puts一下该对象即可。

puts proc {|x| x + 1} # 输出#<Proc:0x00005635d72eb518 /home/hyggge/Desktop/test.rb:1>
puts lambda {|x| x + 1} # 输出#<Proc:0x00005635d72eb310 /home/hyggge/Desktop/test.rb:2 (lambda)>

Block

隐式传递

Block实际上就是一种特殊的Proc,但是它不能独立存在,不能被保存(一次性的,不能赋值给某个变量),只能与函数结合来使用。

def func
    yield 'hyggge'
end

func {|name| puts "hello #{name}"}
    

在这里,我们可以把Block理解成传给函数一个“特殊实参”,不过这种传递是一种“隐式传递”,因为函数不需要设置对应的形参。要想在函数里调用这个Block,我们只需使用yield关键字即可。感觉很像在C语言中把一个函数的指针传递给另一个函数——
void func(void(*block)(char*)) {
    block("hyggge");
}

void block(char* name) {
    printf("hello %s", name);
}

int main() {
    func(block);
    return 0;
}

显式传递

当然,我们也可以在函数的定义中增加一个形参,该形参用于接收要将要传入的块,这种传递就是“显式传递”。需要注意的是,这个特殊形参的前面需要加一个前缀&

def func(&p)
    p.call 'hyggge'
    yield 'hyggge'
end

func {|name| puts "hello #{name}"}

此时,我们既可以用yield关键字来调用块,也可以通过形参p的call方法来调用。

&的使用

在不同的上下文环境下,&的作用和意义也不甚相同,在这里我们只介绍&和Block、Proc相关的一些用法。

&作用于Block

当&作用于Block时,会将Block转化为普通Proc,这其实就是“显式传递”背后的原理。

def func(&p)
    p.call 'hyggge'
    puts p # 输出<Proc:0x000055987014f0b0 /home/hyggge/Desktop/test.rb:6>
end

func {|name| puts "hello #{name}"}

&作用于Proc

如果&作用于Proc(包括普通Proc和lambda),那么它就会将这个Proc转换成一个Block。因为Block是不能独立存在的,所以&proc必须结合函数来使用。

def func
    yield 'hyggge'
end

p = proc {|name| puts "hello #{name}"}
func(&p)

&作用于非Proc对象

如果&作用于非Proc对象(例如Symbol、Hash等等),它会先调用该对象的to_proc方法将该对象转换成Proc,然后再将其转化成一个Block。同样,这个用法也需要结合函数来使用。

def func
    puts yield(1)
end

hash = {1 => 'a', 2 => 'b'}
func(&hash)

下面重点讨论一下&作用于Symbol的情形。我们经常看到这样的代码——

[1, 2, 3].map(&:to_i)

通过上面的讲解我们知道,这里的:to_i首先通过自身的to_proc方法转换成一个Proc对象,然后再转换成一个Block,最后被传入map函数。但问题是,to_proc是如何将:to_i转化成一个Proc的呢?

假设:obj是我们研究的Symbol对象,那么:obj.to_proc的返回值可以理解为proc{|x| x.obj()},这是一种比较直接的理解方式。但是,如果调用者向这个Proc传入的参数(对象)x中没有obj()这样一个成员方法该怎么办呢?换句话说,对象x怎么检查自己有没有obj()这个方法的?

实际上,:obj的to_proc的返回值应该理解为{ |x| x.send(:obj) }。调用send方法时,相当于外界给对象x传入了一个信息,信息的内容是:obj(当然也可以时字符串),如果send方法发现x中有一个和:obj同名的方法,那么这个方法就会接着被调用。


文章作者: Hyggge
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Hyggge !
  目录