Ruby闭包: Block, Proc, lambda

Ruby中的闭包非常强大,也非常重要,所以决定系统的学习下。

闭包(Closure),是指未绑定到任何对象的自由代码,闭包中的代码与任何对象和全局变量无关,只与执行此段代码的上下文相关。

Ruby中闭包的实现有:Block,Proc,Lambada。在学习它们的区别的时候,网上讲的都不是很全面,突然记起vincent在以前的一篇开发周记中专门讲到这个主题,遂又仔细的通读了一遍,讲的非常详细全面,顿时就彻底弄懂了,于是也记录在这里,以便分享与以后查阅。

首先,我们来看Block。

arr = [1,2,3,4,5]  
arr.each { |item| puts item }

这段代码,我们使用了Array对象的block方法,将ary中的每个元素打印出来。从例子中我们可以看到block使用起来很方便,想必传统的Java以及C编码,省略掉了冗余的循环,让你更加专注业务代码,这也是ruby的好处之一。

使用block的最大好处就是可以省略重复的冗余的无用的代码,我们再来看一个读文件的例子:

File.open(__FILE__) do |f|
  puts f.readlines
end

对比用Java代码来读文件,每次都很反感那个冗长的try-catch-finally。从上面的ruby代码中我么可以看到,ruby语言给你做了处理,通过block你可以不用写无用的系统代码了。

从上面的例子我们可以看到,Ruby中的Block对开发者来说,不用再写那些冗余无用的系统代码,而是更加专注业务代码,提升开发效率。

其次,我们再看Proc。

如果你经常使用Block,你会发现一个问题:代码中会有很多重复的Block,比如好多地方需要打印文件内容。怎么解决呢?你第一个想到的是写一个公共函数,不错,可以解决。但是记住你使用的是ruby语言,不要重新造轮子。Ruby提供了函数化的Block:Proc。

我们看一个简单的Proc例子:

p = Proc.new{|f| puts f.readlines}
File.open(__FILE__, &p)

上面例子可以看到,将Block代码定义为一个Proc对象,然后在使用Block的地方用Proc替换,这样就做到了完美的代码复用。

最后我们看看lambda。

Lambda:拉姆达表达式,ruby中可以通过lambda表达式创建Proc对象,可以把它理解成一种匿名函数。我们先看例子:

new_p = lambda{|f| puts f.readlines}
File.open(__FILE__, &new_p)

上面例子中,我们使用系统的lambda方法创建了一个Proc对象,其效果与Proc.new创建出来的是一样的。其实使用lambda与使用Proc.new效果基本是一样的。下面就来讲下他们的区别:

yield 和 block call 的区别

yield 和 block call 两种都能实现运行闭包,从实际运行效果来说,区别不大。其区别主要在于:

1. 闭包的传递和保存

因为闭包已经传递到参数里,所以可以继续传递或保存起来,例如:

class A
  def foo1(&b)
    @proc = b
  end

  def foo2
    @proc.call if @proc
  end
end
   
a = A.new
a.foo1 { puts "proc from foo1" }
a.foo2

2. 性能

yield不是方法调用,而是 Ruby 的关键字,yield 要比 block call 比快 1 倍左右。

block 和 proc, lambda 的区别

很简单直接,引入 proc 和 lambda 就是为了复用,避免重复定义,例如在上例中,使用 proc 变量存储闭包,避免重复定义两个 block 。

proc 和 lambda 的区别

这大概是最让人困惑的地方,从行为上看,他们的效果是一致的,为什么要用两种不同的表达方式。

proc = Proc.new { puts "foo in proc" }
lambda_proc = lambda { puts "foo in lambda" }

确实,对于简单的情况,他们的行为是一致的,但是主要在两个地方有明显差异:

1. 参数检查,lambdas检查参数的个数,Procs不会。

还是例子说话

def foo
  x = 100
  yield x
end

proc = Proc.new { |a, b| puts "a is #{a.inspect} b is #{b.inspect}" }
foo(&proc)           #=> a is 100 b is nil

lambda_proc1 = lambda { |a| puts "a is #{a.inspect}" }
foo(&lambda_proc1)   #=> a is 100
lambda_proc2 = lambda { |a, b| puts "a is #{a.inspect} b is #{b.inspect}" }
foo(&lambda_proc2)   #=> ArgumentError: wrong number of arguments (1 for 2)

可见,proc 不会对参数进行个数匹配检查,而 lambda 会,如果不匹配还会报异常,所以安全起见,建议优先用 lambda。

2. return不同。lambdas的return是返回值给方法,方法会继续执行。 Proc的return会终止方法并返回得到的值。

还是例子说话

def foo
	f = Proc.new { return "return from foo from inside proc" }
	f.call # control leaves foo here
	return "return from foo"
end

def bar
  f = lambda { return "return from lambda" }
  puts f.call # control does not leave bar here
  return "return from bar"
end

puts foo  #=> return from foo from inside proc

puts bar  #=> return from lambda
          #=> return from bar

可见,proc 中,return 相当于执行了闭包环境里的 return,而 lambda 只是返回call 闭包所在环境。

为什么会有这样的不同?

答案在于procedures和methods概念上的不同。 Ruby中的Procs是代码片段(code snippets),不是方法。因此,Proc的return就是整个方法的return。 但lambdas就像是单独的methods(只不过是匿名的),所以它要检查参数个数,且不会覆盖整个方法的返回。 因此,最好把lambdas当作另一种methods的写法,一种匿名的方式。

总结

闭包是 Ruby 的强大特性,它的几种表达方式block,proc 和 lambda有细微差别。blocks和Procs看起来像在代码中插入代码片段。而lambdas和Methods看起来像方法。通过几个例子和比较,希望能了解如何灵活运用闭包,游刃有余!


Ruby Range

最近一直在做“生理周期”,不过不要担心,这里只会探讨技术领域,不会涉及到神圣的MC领域。在做生理周期指数预测、周期日报的时候大量用到了Range对象,所以干脆就把Range对象系统的学习下,在这里记录并分享出来。

Ruby Range

Range在概念上很直观,即范围,但是在实际使用中可能会遇到一些容易混淆的东西,看下面代码:

# 注意:..操作符将包含上限,而...不包含上限。
(-1 .. -5).to_a      #=> []
(-5 .. -1).to_a      #=> [-5, -4, -3, -2, -1]
('a' .. 'e').to_a    #=> ["a", "b", "c", "d", "e"]
('a' ... 'e').to_a   #=> ["a", "b", "c", "d"]

begin/end first/last

使用first和last方法(或同义方法begin和end),可以获取一个Range的开始和结束元素:

r1 = 3 .. 6
r2 = 3 ... 6
r1a, r1b = r1.first, r1.last    #=> 3, 6
r1c, r1d = r1.begin, r1.end     #=> 3, 6
r2a, r2b = r2.begin, r2.end     #=> 3, 6 (注意:不是3和5)
r1.first(2)                     #=> [3, 4]

exclude_end?

exclude_end?方法可以得到Range是否排除上限项(即是否是…的Range)

r1 = 3 .. 6
r2 = 3 ... 6
r1.exclude_end?   #=> false
r2.exclude_end?   #=> true

include?/cover?

include?方法(同义方法member?)或者cover?方法可以判断一个值是否处在当前的Range中:

r = 1 .. 5
r.include?(1)     #=> true
r.include?(0)     #=> false
r.cover?(1)       #=> true
r.cover?(0)       #=> false

区别:include?方法是把Range的元素迭代取出进行比较,而cover?方法只是把给出的值和该Range的上下限做比较得出的,所以cover?性能比include?要好。

step

如果我们想要从0 .. 20中取出这样的元素0,5,10,20怎么办?这时候step方法就用到了

r = 0 .. 20
r.step(5).to_a     #=> [0, 5, 10, 15, 20]

浮点数的Range

浮点数的Range可以进行迭代么?我们来看一下:

fr = 2.0..2.2
fr.each {|x| puts x }   #=> TypeError: can't iterate from Float

为什么浮点数不可以迭代呢?是因为不能实现么?理论上,这是没有问题的,但是,实际上,如果浮点数Range迭代,这有可能出现:很小的一个范围,将产生非常庞大的迭代量。这对语言的实现有非常高的要求。况且,这样的功能,极少有用到。

反向的Range

我们讨论下限大于上限的Range,如:

r = 6..3
x = r.begin              #=> 6
y = r.end                #=> 3
flag = r.end_excluded?   #=> false

它确实是个合法的Range,但是,它包含的内容却并不是我们想像的那样:

arr = r.to_a       #=> []
r.each {|x| p x}   #=> 无结果
r.include?(5)      #=> false

那么说反向Range是没有什么用处的咯?那倒不是,我们可以在字符串和数组中使用反向Range:

string = "flowery"
str1   = string[0..-2]   #=> "flower"
str2   = string[1..-2]   #=> "lower"
str3   = string[-5..-3]  #=> "owe" (其实这是个正向的Range)

Become a Better Developer You Can

很荣幸参加了RubyConfChina-2012大会,即第四次Ruby中国大会,第一次参加难免有点激动, 更何况还见到了很多国内外的大牛,这次讲师们的演讲也非常精彩,这篇Blog就来整理下Fred Wu的分享, 感觉对以后的编程道路有很好的帮助作用。

Three Stages Of Programmer

1.You discuss tools and editors.三流程序员谈工具

2.You discuss programming techniques.二流程序员谈技术

3.You discuss creative and thought processess.一流程序员谈思想

Thoughts On Software Development

  • Second system syndrome - avoid total rewrites.

第二系统综合症——避免重写

  • Software engineering is like playing Warcraft - always check your mini-map.

软件工程有如魔兽争霸——需经常关注小地图,以大局为重

  • Experience is as important as talent, if not more important.

经验与天赋同重要

  • Technical debt is a debt, the longer you wait, the more interests you play.

技术债务——拖欠越久,偿还越多

  • Choose the application architecture wisely.

谨慎选择软件架构

Four Simple Rules

Passed all the tests.

Contains no duplication.

Expresses the intent.

Minimises the number of classes and methods.

Contribution To Open Source Projects

Reading code is just as important as writing code.

阅读代码与编写代码同重要

Learn from the masters.

向大师们学习

Take some, give some - sharing is awesome.

不断收获的同时,别忘了付出

Making things more useful for yourself therefore for others too.

对自己有用的东西,也许对别人也有用

Refactor In The Wild

Improve code readability and maintainability

A Simple Example

def self.search(options = {})
  scope = scoped

  if options.has_key?(:employee_groups)

    employee_group_ids = Array.wrap(options[:employee_groups]).map(&:id)

    scope = scope.includes(:employee_group_dashboard_links)
      .where(:employee_group_dashboard_links => {
      	:employee_group_id => employee_group_ids
      	})
  end

  scope
end

After Refactor

def self.search(options = {})
  result = if options.has_key?(:employee_groups)
    scoped.includes(:employee_group_dashboard_links)
      .where(:employee_group_dashboard_links => {
        :employee_group_id => Array.wrap(options[:employee_groups]).map(&:id)
      })
  end

  result || scoped
end

A Ruby-China Example

# 检查用户是否看过
# result:
#   0 读过
#   1 未读
#   2 最后是用户的回复
def user_readed?(user_id)
  uids = Rails.cache.read("Topic:user_read:#{self.id}")
  if uids.blank?
    if self.last_reply_user_id == user_id || self.user_id == user_id
      return 2
    else
      return 1
    end
  end

  if uids.index(user_id)
    return 0
  else
    if self.last_reply_user_id == user_id || self.user_id == user_id
      return 2
    else
      return 1
    end
  end
end

After Refactor

# 检查用户是否看过
# result:
#   0 读过
#   1 未读
#   2 最后是用户的回复
def user_readed?(user_id)
  user_ids = Rails.cache.read("Topic:user_read:#{self.id}") || []

  if user_id.in?(user_ids)
    0
  elsif user_id.in?([self.last_reply_user_id, self.user_id])
    2
  else
    1
  end
end

搭建Octopress

准备开一个博客,但是一直犹豫在哪里开,是在CSDN,博客园,新浪,网易…纠结中,但是一直觉得以上平台要么不适合做技术博客,要么觉得不太高端,门槛太低。于某一天终于发现了Octopress,欣喜过望,这就是我想要的,有一定门槛,需要ruby,git等技术。终于可以像黑客一样写博客了,哈哈,很兴奋。下面记录下搭建的过程。

1.安装Octopress

git clone git://github.com/imathis/octopress.git octopress
cd octopress
bundle update    # 安装依赖的组件
rake install     # 安装默认的Octopress主题

2.配置Git

值得注意的是这里git的origin已经存在,并且指向octopress的master分支的,这里为了方便进行了更改:

git remote rm origin
git remote add origin git@github.com:stormzhang/stormzhang.github.com.git
git remote add octopress git://github.com/imathis/octopress.git  # 为了octopress的升级而添加

3.配置github

在github上创建一个仓库,注意仓库名称要以下这种格式yourname.github.com,这样代码发布后自动这个url就可以访问了(此处一定要注意哦,我刚开始没注意,死活没得到想要的效果)。 例如你的 GitHub 帐号是 jack 就将 Repository 命名为 jack.github.com, 完成后会得到一组 GitHub Pages URL http://yourname.github.com/ (注意不能用 https协议,必须用 http协议)。

设定 GitHub Pages

rake setup_github_pages

以上执行后会要求 read/write url for repository :

git@github.com:yourname/yourname.github.com.git

rake generate
rake deploy

等待几分钟后,github上会收到一封信:“{yourname.github.com} Page build successful”,第一次发布后等比较久,之后每次都会直接更新。 当你发布之后,你就可以到 http://yourname.github.com 上看到你的博客了.

4.将 source 也加入 git

git add .
git commit -m 'initial source commit'
git push origin source

5.更新 Octopress

git remote add octopress git://github.com/imathis/octopress.git
git pull octopress master     # Get the latest Octopress
bundle install                # Keep gems updated
rake update_source            # update the template's source
rake update_style             # update the template's style

6.发表新文章

rake new_post["新文章名称"]
rake preview

用浏览器打开 http://localhost:4000 就可以看到效果了。

7.发布

rake gen_deploy
rake deploy                 #若发布后无效果可试试此命令