元编程-代码块

基础知识

概念:

  1. 块由大量代码构成
  2. 块定义在{} 中,或者do...end关键字中
  3. 使用yield 语句来调用块
  4. 块与其具有相同名称的函数调用
  5. 调用一个方法时才可以定义一个块
  6. 可以使用kernel中的block_given?() 来询问当前方法的调用是否包含块
  7. 块有自己的参数,回调块时可以像调用方法一样为块提供参数参数

作用域

作用域是变量和方法可用性的范围

闭包

  1. 块是完整的,可以立即运行
  2. 块可以包含代码,也可以包含一组绑定
  3. 创建块时会获取到局部绑定,调用时会将块和其绑定传给方法
def method
    a = 1
    yield('whj')
end

a = 'hello'
method {|b| "#{a}, #{b}" }
=> "hello, whj"

这里在定义块 时 {|b| "#{a}, #{b}" } a 还是 'hello', 在yield 调用块时将 'hello'带入到方法中
  1. 块中额外的绑定会在块结束时消失(额外的绑定作用域仅限于块中)

切换作用域

全局变量可以在任何作用域中使用,但一当某处发生了修改,难以定位到,所以能不用全局变量就不用全局变量
建议用顶级变量代替全局变量,顶级变量: 对象main的实例变量
使用方法调用来代替作用域门

作用域门

程序会在三个地方切换作用域

  1. 类定义 class
  2. 模块定于 module
  3. 方法定义 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对象

扁平化作用域(嵌套文法作用域)

使绑定穿越作用域门

  1. 将作用域门替换为非作用域门

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"

区别

  1. lambda方法创建的Proc对象称为lambda, 其他方法创建的称为proc(Proc#lambda? 方法检测是否为lambda)
  2. 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

  1. 参数校验

lambda 参数校验严格,如果参数个数不对,会抛出ArgumentError

proc 会根据传入的参数调整成期望的参数形式,多的入参去掉, 少的入参补 nil

方法

通过调用kernel#method方法,可以获得一个用Method对象表示的方法,用Method#call 对其调用

Method#to_proc 可以将Method对象转化为Proc

&操作符

yield 不适用于:

  1. 把代码块传递给另外一个方法或者代码块
  2. 将代码块转为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和块对参数较宽容

你可能感兴趣的:(元编程-代码块)