Ruby 基础+进阶 第四天

本次学习资料

通读《Ruby元编程》,记录下有趣的以及不明白的知识点

代码块

代码块的定义方式有{}花括号与do…end关键字定义两种,单行用花括号,多行用do…end

代码块只有在方法调用的时候才可以定义,块会被直接传递给这个方法,判断某个方法调用中是否包含代码块,可以通过Kernel#block_given?

代码块不仅可以有自己的参数,也会有返回值,往往代码块中的最后一行执行结果会被作为返回值返回

代码块之所以可以执行,是因为其不仅包含代码,同时也涵盖一组相应绑定,即执行环境,也可以称之为上下文环境。代码块可以携带这个上下文环境,到任何一个代码块可以达到的地方。也就可以说,一个代码块是一个闭包,当定义一个代码块时,它会捕获当前环境中的绑定,并带它们四处流动。

绑定与闭包

可以运行的代码由两部分组成:代码本身和一组绑定。运行代码需要一个执行环境:局部变量、实例变量、self等,这些东西都是绑定在对象上的名字,所以把他们简称为绑定。

如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被定义为闭包 。代码块可以获取局部绑定并一直带着他们,因此也被称为闭包。

作用域

Ruby中不具备嵌套作用域(即在内部作用域,可以看到外部作用域的)的特点,它的作用域是截然分开的,一旦进入一个新的作用域,原先的绑定会被替换为一组新的绑定。

程序会在三个地方关闭前一个作用域,同时打开一个新的作用域,它们是:
类定义class
模块定义 module
方法定义 def
上面三个关键字,每个关键字对应一个作用域门(进入),相应的end则对应离开这道门。

扁平化作用域

从一个作用域进入另一个作用域的时候,局部变量会立即失效,为了让局部变量持续有效,可以通过规避关键字的方式,使用方法调用来代替作用域门,让一个作用域看到另一个作用域里的变量,从而达到目的。具体做法是,通过Class.new替代class,Module#define_method代替def,Module.new代替module。这种做法称为扁平作用域,表示两个作用域挤压到一起。

示例代码(Wrong)

my_var =Successclass MyClass
    puts my_var  #这里无法正确打印”Success”
    my_var =Successdef my_method
        puts my_var  #这里无法正确打印”Success”
    end
end

示例代码(Right)

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
obj = MyClass.new
obj.my_method
#=>Success in  the class definition
#=>Success in the method

共享作用域

将一组方法定义到,某个变量的扁平作用域中,可以保证变量仅被有限的几个方法所共享。这种方式称为共享作用域。

class MyClass
  my_var = "Success"
  define_method :my_method1 do
    puts "#{my_var} in the method1"
  end
  define_method :my_method2 do
    puts "#{my_var} in the method2"
  end
end
obj = MyClass.new
obj.my_method1
obj.my_method2
#=>Success in the method1
#=>Success in the method2
#利用好作用域门和扁平化作用域就可以让my_var只被几个方法共享

instance_eval方法

这个BasicObject#instance_eval有点类似JS中的bind方法,不同的是,bind是将this传入到对象中,而instance_eval则是将代码块(上下文探针Context Probe)传入到指定的对象中,一个是传对象,一个是传执行体。通过这种方式就可以在instance_eval中的代码块里访问到调用者对象中的变量。

示例代码

class MyClass
    def initialize
        @v = 1
    end
end
obj = MyClass.new

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

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

此外,instance_eval方法还有一个双胞胎兄弟:instance_exec方法。相比前者后者更加灵活,允许对代码块传入参数。

示例代码

class C
    def initialize
        @x = 1
    end
end
class D
    def twisted_method
        @y = 2
        #C.new.instance_eval { “@x: #{@x}, @y>: #{y}” }
        C.new.instance_exec(@y) { |y|@x: #{@x}, @y: #{y}” }
    end
end
#D.new.twisted_method   # => “@x: 1, @y: ”
D.new.twisted_method   # => “@x: 1, @y: 2”

因为调用instance_eval后,将调用者作为了当前的self,所以作用域更换到了class C中,之前的作用域就不生效了。这时如果还想访问到之前@y变量,就需要通过参数打包上@y一起随instance_eval转义,但因为instance_eval不能携带参数,所以使用其同胞兄弟instance_exec方法。

洁净室

一个只为在其中执行块的对象,称为洁净室,其只是一个用来执行块的环境。可以使用BasicObject作为洁净室是个不错的选择,因为它是白板类,内部的变量和方法都比较有效,不会引起冲突。

Proc对象

Proc是由块转换来的对象。创建一个PRoc共有四种方法,分别是:

示例代码

#法一
inc = Proc.new { | x | x + 1}
inc.call(2)  #=> 3
#法二
inc = lambda {| x | x + 1 }
inc.call(2)  #=> 3
#法三(等同法二)
inc = ->(x) { x + 1}
inc.call(2) #=> 3
#法四
inc = proc {|x| x + 1 }
inc.call(2) #=> 3

除了上面的四种之外,还有一种通过&操作符的方式,将代码块与Proc对象进行转换。如果需要将某个代码块作为参数传递给方法,需要通过为这个参数添加&符号,并且其位置必须是在参数的最后一个

&符号的含义是: 这是一个Proc对象,我想把它当成代码块来使用。去掉*&符号,将能再次得到一个Proc对象。

示例代码

def my_method(&the_proc)
    the_proc
end

p = my_method {|name|Hello, #{name} !”}
p.class   #=> Proc
p.call(Bill)   #=> “Hello,Bill”
def my_method(greeting)#{greeting}, #{yield}!”
end

my_proc = proc {Bill}
my_method(Hello, &my_proc)

Proc与Lambda对比

使用Lambda方法创建的Proc与其它方式创建的Proc是有一些差别的,用lambda方法创建的Proc称为lambda,而用其他方式创建的则称为proc。通过Proc#lambda?可以检测Proc是不是lambda。

二者之间主要的差异有以下两点:

Proc、Lambda的return含义不同; lambda中的return表示的仅仅是从lambda中返回。而proc中,return的行为则不同,其并不是从proc中返回,而是从定义proc的作用域中返回。即相当与在你的业务代码处返回。
示例代码

def double(callable_object)
    p = Proc.new { return 10 }
    result = p.call   
    return result * 2 # 不可达的代码
end

double #=> 10

ProcLambdareturn参数数量

可调用对象

代码块(不是真正的对象,但是它们是”可调用的”):在定义它们的作用域中执行
proc: Proc类的对象与代码块一样,也在定义自身的作用域中执行
lambda: 同样是Proc类的对象,但跟普通的proc有细微差别。与代码块与proc一样都是闭包,也在定义自身的作用域中执行
方法: 绑定于一个对象,在 所绑定对象的作用域中执行。他们也可以与这个作用域解除绑定,然后再重新绑定到另一个对象的作用域中
前三个在之前的笔记中都有详细介绍,今天来详细看下最后一个Method对象部分

Method 对象

通过Kernel#method方法,可以获得一个用Method对象表示的方法,在之后可以用Method#call方法对其进行调用。同样也可以用Kernel#singleton_method方法把单件方法名转换为Method对象

示例代码

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

自由方法

与普通方法类似,不同的是它会从最初定义它的类或模块中脱离出来(即脱离之前的作用域),通过Method#unbind方法,可以将一个方法变为自由方法。同时,你也可以通过调用Module#instance_method获得一个自由方法.

示例代码

module MyClass
    def my_method
        42
    end
end

unbound = MyModule.instance_method(:my_method)
unbound.class  #=> UnboundMethod

上面代码中的UnboundMethod对象,不能够直接用来调用,不过可以将一个UnboundMethod方法绑定到一个对象上,使其再次成为一个Method对象,然后再被调用执行。

上面的绑定过程可以通过UnboundMethod#bind方法把UnboundMethod对象绑定到一个对象上,从某个类中分离出来的UnboundMethod对象只能绑定在该类及其子类对象上,不过从模块中分离出来的UnboundMethod对象在Ruby2.0后就不在有此限制了。

此外还可以将UnboundMethod对象传递给Module#define_method方法,从而实现绑定。

String.send :define_method, :another_method, unbound
“abc”.anther_method  #=> 42

类的返回值

像方法一样,类定义也会返回最后一条语句的值:

result = class MyClass
self
end
result #=> MyClass

当前类

与当前对象self一样,同时还存在当前类(或模块)

Ruby中并没有类似当前对象self一样的明确引用,不过在追踪当前类的时候,可以遵循下面几条:

在程序顶层,当前类是Object,这是main对象所属的类。
在一个方法中,当前类就是当前对象的类。(在一个方法中用def关键字定义另一个方法,新定义的方法会定义在self所属的类中。因为Ruby解释器总是追踪当前类(或模块)的引用,所以使用def定义的方法都成为当前类的实例方法了)
示例代码

class C
    def m1
        def m2; end
    end
end

class D < C; end

obj = D.new
obj.m1

C.instance_methods(false)   #=> [:m1, :m2]

当用class关键字打开一个类时(或module关键字打开模块),那个类称为当前类。

class_eval方法

Module#class_eval方法(别名: module_eval),会在一个已存在的类的上下文中执行一个块。使用该方法可以在不需要class关键字的前提下,打开类。

Module#class_eval与Object#instance_eval方法相比,后者instance_eval方法只能修改self,而class_eval方法可以同时修改self与当前类。
对象 不能调用 class_eval;
对象调用 instance_eval 的作用域为对象实例本身;其中定义的方法为单例方法;
类调用 class_eval 的作用域为类本身;其中定义的方法为实例方法;
类调用 instance_eval 的作用域为 class 对象,其中定义的方法为类方法;

此外class_eval的另一个优势就是可以利用扁平作用域,规避class关键字的作用域门问题。

instance_eval 与 class_eval的选择

通常使用instance_eval方法打开非类的对象,而用class_eval方法打开类定义,然后用def定义方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值