基础知识
概念:
- 块由大量代码构成
- 块定义在{} 中,或者do...end关键字中
- 使用yield 语句来调用块
- 块与其具有相同名称的函数调用
- 调用一个方法时才可以定义一个块
- 可以使用kernel中的block_given?() 来询问当前方法的调用是否包含块
- 块有自己的参数,回调块时可以像调用方法一样为块提供参数参数
作用域
作用域是变量和方法可用性的范围
闭包
- 块是完整的,可以立即运行
- 块可以包含代码,也可以包含一组绑定
- 创建块时会获取到局部绑定,调用时会将块和其绑定传给方法
def method
a = 1
yield('whj')
end
a = 'hello'
method {|b| "#{a}, #{b}" }
=> "hello, whj"
这里在定义块 时 {|b| "#{a}, #{b}" } a 还是 'hello', 在yield 调用块时将 'hello'带入到方法中
- 块中额外的绑定会在块结束时消失(额外的绑定作用域仅限于块中)
切换作用域
全局变量可以在任何作用域中使用,但一当某处发生了修改,难以定位到,所以能不用全局变量就不用全局变量
建议用顶级变量代替全局变量,顶级变量: 对象main的实例变量
使用方法调用来代替作用域门
作用域门
程序会在三个地方切换作用域
- 类定义 class
- 模块定于 module
- 方法定义 def
#kernel 中的local_variables 可以看到绑定
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]
local_variables # [:v1, :obj] 绑定为最上面的v1和上面定义的obj对象
扁平化作用域(嵌套文法作用域)
使绑定穿越作用域门
- 将作用域门替换为非作用域门
Class.new 是class的完美替身
a = 'hello'
MyClass = Class.new do
"#{a}" #相当于创建了一个块,a是块的局部绑定
end
Module#define_method 方法来替代def
a = 'hello'
define_method :my_method do
"#{a}"
end
共享作用域
扁平作用域中定义了多个方法,将这些方法用一个作用域门保护起来,他们就可以共享绑定
def my_method
a = 'hello'
Kernel.send :define_method, :test_a do
a
end
Kernel.send :define_method, :test_b do |x|
a = a + x
end
end
my_method
test_a # => 'hello'
test_b('whj') => 'hello whj'
使用send 来调用kernel的私有方法 define_method
kernel#test_a 和kernel#test_b 都可以看到a变量
其他地方因def这个作用域门的原因是看不到a的
instance_eval 方法
BasicObject#instance_eval, 在一个对象的上下文中执行块
obj.instance_eval do
self # => obj
end
instance_eval 会将代码块的接受者置为self,因此它可以访问接受者的私有方法和实例变量
instance_exec
instance_eval 不能够接受参数
instance_exce 可以接受参数,更灵活一些
洁净室
只是为了在其中执行块的对象,能够提供有用的方法来供代码块使用
可调用对象
块 (并不是对象,但是可以调用)
proc: 由块转换来的对象,Proc类
lambda: proc的变种,Proc类
方法
想存储一个代码块供以后使用就需要一个对象,将代码块传给一个proc对象,之后就可以用Proc#call 来执行这个代码块
str = Proc.new {|x| "#{x}, whj"}
str.call("hello") # => "hello, whj"
proc 和 lambda
proc 和lambda 是ruby中的两个内核方法,能够将代码块转为Proc对象
str = lambda {|x| "#{x}, whj"}
str = -> (x) {"#{x}, whj"} # lambda的第二种方式
str = proc {|x| "#{x}, whj"}
str = Proc.new {|x| "#{x}, whj"}
str.call("hello") # => "hello, whj"
区别
- lambda方法创建的Proc对象称为lambda, 其他方法创建的称为proc(Proc#lambda? 方法检测是否为lambda)
- return
lambda中 return仅仅表示从lambda中退出
def sum(obj)
obj.call * 2
end
num = lambda { return 10 }
sum(num) # => 20
proc 中return 是从定义proc的定义域中返回
def sum
obj = proc {return 10}
num1 = obj.call # 此时直接返回,不再继续往下
num1 * 2
end
sum # => 10
- 参数校验
lambda 参数校验严格,如果参数个数不对,会抛出ArgumentError
proc 会根据传入的参数调整成期望的参数形式,多的入参去掉, 少的入参补 nil
方法
通过调用kernel#method方法,可以获得一个用Method对象表示的方法,用Method#call 对其调用
Method#to_proc 可以将Method对象转化为Proc
&操作符
yield 不适用于:
- 把代码块传递给另外一个方法或者代码块
- 将代码块转为Proc
此时需要给代码块取一个名字,并将代码块附加到一个绑定上,可以给这个方法添加一个特殊的参数,这个参数必须是参数列表的最后一个,且以&开头
&操作符的含义: 这是一个Proc对象, 当作代码块来使用,去掉&操作符,就能得到一个Proc对象
def my_method(&proc)
proc
end
p = my_method {|x| "#{x}, whj"}
p.class # => Proc
p.call("good night") # => "good night, whj"
将Proc转为代码块只需要在proc对象前面加上&
结论
lambda 更直观,像一个方法,且调用return只从代码中块返回
方法最严格,绑定于一个对象,在对象的作用域中执行
proc和块对参数较宽容