本章内容理解:讲了打破封装,随时随地增加或修改变量和方法的技术。
代码块作为“程序中的基本操作元素之一”,是实现下面这些操作的基础:
代码块是闭包,闭包的作用在于,让变量在指定的作用域发挥作用,不会受到作用域外变量的干扰。也在于将变量带到别的作用域。
接下来依次记录块,作用域以及打破封装共享变量的技术操作。
块
|
|
通常情况下,如果只有一行,代码块可以通过大括号定义,比如上面的{|x, y| (x + y) * 3 },若有多行,可以通过def...end来定义。
在调用一个方法时才可以定义一个块。块会被直接传递给这个方法,该方法可以用yield关键字调用这个块。
通过Kernel#block_given?()方法来询问当前的方法调用是否包含块。
|
|
可以运行的代码由两部分组成:代码本身和一组绑定。
从上面两个例子可以看出,代码块是闭包,它可以把变量带出原来的作用域,带入a_method中执行。代码块不能孤立地运行,它需要一个执行环境:局部变量,实例变量,self等。可运行的代码包括两部分:代码本身和一组绑定。那么代码块是如何获得一组绑定的呢?
|
|
创建代码块时,代码块会获得局部绑定,然后将这两者一起传给一个方法。
还可以在代码块内定义额外的绑定,但这些绑定在代码块结束时就消失了。
|
|
基于代码块可以获取局部绑定并一直携带它们的特性,那应该如何使用闭包呢?接下来就要理解作用域了。局部变量之所以被称为局部变量,也是因为它只在自己的作用域内有效。
作用域
作用域(scope),程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
作用域通过作用域门来划分。程序会在作用域门关闭前一个作用域,同时打开一个新的作用域,作用域门有三处地方:
1.类定义。
2.模块定义。
3.方法。
切换作用域下面的例子演示了在程序运行时作用域是怎样切换的,它会用Kernel#local_variables()方法来跟踪绑定的名字:
|
|
在一些语言中,比如Java和C#,有“内部作用域(innerscope)”的概念。在内部作用域中可以看到“外部作用域(outerscope)”中的变量。但Ruby中没有这种嵌套式的作用域,它的作用域之间是截然分开的:一旦进入一个新的作用域,原先的绑定就会被替换为一组新的绑定。这意味着在程序进入MyClass后,v1便“超出作用域范围”,从而就不可见了。
几种变量及其作用域:
全局变量
顶级实例变量
局部变量
123456789101112131415161718192021222324252627282930313233343536373839 > class A> $var1 = 2 # => 全局变量> @var = "the top level variable" # =>顶级实例变量>> def my_method> puts @var> var2 = 3> end>> def my_method1> puts var2> # => NameError: undefined local variable or method `var2' for> # # <A:0x007ffc8f8bfe00> 在my_method中定义的局部变量不能在my_method1中使用> end> end>> class B> def my_method> puts $var1> end>> def my_method1> puts @var2> end> end>> a = A.new> #=> #<A:0x007ffd08900328>> a.my_method> # 没有输出,要由main扮演self的角色,才能有用。而这里的self是对象a> # 所以去掉classA,直接在irb中运行这段代码才可以输出@var>> b = B.new> #=> #<B:0x007fb62584bb78>> b.my_method> #=> 2 # 全局变量可以在任何作用域中访问,所以即使$var1定义在A中,B的my_method方法也可以输出它> b.my_method1> # 没有输出>
>
全局变量的问题在于系统的任何部分都可以修改它们。因此,你会立即发现几乎没法追踪谁把它们改成了什么。正因为如此,基本的原则是:如非必要,尽可能少使用全局变量。
你有时可以用顶级实例变量来代替全局变量,只要main对象在扮演self的角色,就可以访问一个顶级实例变量。但当其他对象成为self时,顶级实例变量就退出作用域了。
如果希望让一个变量穿越作用域,那么该怎么做呢?要解答这个问题,还是得回到块的主题上。
如何改变作用域的范围?
1.扁平化作用域:
|
|
class这个作用域门。虽然不能让my_var穿越它,但是可以把class关键字替换为某个非作用域门的东西:方法。如果能用方法替换class,就能在一个闭包中获得my_var的值,并把这个闭包传递给该方法。
所以上面的代码可以这样修改:
|
|
这里用了Class.new以及define_method实现扁平作用域,从而实现了变量共享。
扁平化作用域,顾名思义就是把作用域挤压在一起,共享变量。
2.共享作用域
假定你想在一组方法之间共享一个变量,但是又不希望其他方法也能访问这个变量,就可以把这些方法定义在那个变量所在的扁平作用域中,这时这个扁平作用域中也叫做共享作用域。
|
|
用自己的话整理了改变作用域操作代码块的操作:
1.更改一个类中的实例变量
|
|
2.传递一个块到一个方法中
|
|
这里就把{|x, y| (x + y) * 3 }传递到a_method方法中,然后用yield求出程序的最终结果。
3.传递一个块的结果到一个到另一个块中
|
|
但是instance_eval无法传递参数,要改用instance_exec,下面的例子比较了它们之间的区别
|
|
剖析代码块的底层
从底层看,使用代码块分为两步,一是将代码块打包备用,二是是执行被打包的代码。
这些打包备用的东西就是可调用对象:
1.块
2.使用proc。proc基本上就是一个由块转换成的对象。
3.使用lambda。它是proc的近亲。
4.使用方法。
Proc对象
尽管Ruby中绝大多数东西都是对象,但是块不是。为什么要关心这个呢?设想希望存储[一个块供以后执行],这时,你需要一个对象才能做到。Bill说道,“为了解决这个问题,Ruby在标准库中提供了名为Proc的类。”一个Proc就是一个转换成对象的块。
|
|
这种先打包代码,以后调用的技术称为延迟执行(Deferred Evaluation)。
|
|
&操作符
&操作符能把代码块传递给另一个方法或者代码块。
|
|
&操作符会把proc对象my_proc转换为块,再把这个块传给这个方法
|
|
Proc和lambda的区别:
1.对return的处理不同。
|
|
2.对参数检查的容忍度不同。
|
|
proc根据实际参数的数量自动调整输出结果。再来看lambda
|
|
如果参数个数不恰好是两个,就会报错。
方法也是一个可调用对象:
|
|
这章知识的运用:
开发一个DSL语言,进行几次重构,使代码质量不断进阶:
1.作用域共享
2.变量不要胡乱地散落在顶级作用域里
3.把事件触发条件从代码块转换成proc
4.消灭全局变量,使用共享作用域
5.增加洁净室