Ruby中的一些惯用技巧

说三个自己也用过的技巧,没用过的也写不出来。

空指针保护

这个非常常见。

看个例子:

a ||= []
## 上面的|| 是逻辑操作符或,相当于 a || (a = [])

上面这个方法的好处在于,它可以保证a不为nil,这种惯用方法称为空指针保护

空指针保护常常用于初始化实例变量。比如下面的例子:

class C
  def data
    @data ||= {}
  end

  def data?
    @data.present?
  end
end

Tap

《ruby元编程》里面提到了tap,使用tap,可以在方法链中插入中间操作,比如:

temp = ['a','b','c'].push('d').shift
puts temp #=> a
x = temp.upcase.next

上诉的代码显得很不ruby,使用tap改进下,可以这么写:

['a','b','c'].push('d').shift.tap {|x| puts x}.upcase.next #=> a

其实tap在方法定义中也很常见。比如i18n-tasks的源代码中,就多次出现了tap的身影:

举一个简单的例子感受下:

# source是一个Array,存放文件路径
# get_all_files 可以获取指定的单个或多个路径下所有的文件
def get_all_files(source)
  files = []
  source.map do |path|
    path = path[-1] == '/' ? path : path + '/'
    group = Dir.glob("#{path}**/**")
    files << group.reject { |x| File.directory?(x) }
  end
end

如果换成tap,则可以这样定义:

def get_all_files(source)
  [].tap do |files|
    source.map do |path|
      path = path[-1] == '/' ? path : path + '/'
      group = Dir.glob("#{path}**/**")
      files << group.reject { |x| File.directory?(x) }
    end
  end
end

Symbol#to_proc

这个在rubyist高手中算是比较流行的一种技巧了。

看个常用的数组求和:

a = [1,2,3,4]
a.reduce(&:+) #= > 10
a.inject(0,&:+) #= > 10

「reduce 和inject 是alias的关系, 两者可以互用。我依然记得第一次看到数组求和[1,2,3,4].reduce(&:+) 这个例子时,一脸懵逼……」

这种带有&:+的标记的,就是Symbol#to_proc。

上面的求和,如果不用Symbol#to_proc, 则可以这样写:

a = [1,2,3,4]
a.reduce{|sum, x| sum + x } #= > 10
a.inject(0){|sum, x| sum + x } #= > 10

对于Symbol#to_proc 的方法的使用,要注意一点,那就是&:method中的method,必须是对象可以调用的无参method,比如上面的&:+,+ 是1,2,3,4都可以调用的无参实例方法

看个例子感觉下:

b = ['hi','ruby','hello']
b.map(&:capitalize) #= > ["Hi", "Ruby", "Hello"]
b.map(&:nil?) #= > [false, false, false]
b.map(&:concat('world')) #= > raise: syntax error

b中的每一个元素都是String对象,可以成功调用无参的实例方法capitalize,nil?等,但是当调用带有参数的concat方法时就会报错,为什么?

原来在Symbol类中有一个to_proc方法,用于把符号转化为一个Proc对象:

class Symbol
  def to_proc
    Proc.new {|x| x.send(self)} # 动态派发,调用self这个method的
  end
end

上面的b.map(&:capitalize) 等价于b.map(&:capitalize.to_proc), 由于&可以调用对象的to_proc方法把对象转为一个proc,所以可以简写成b.map(&:capitalize) , 也就是说,b.map(&:capitalize) 等同于b.map{|x|x.send(:capitalize)}

注意到Symbol类中定义的to_proc方法是不带参数的。

当然,如果你想要调用带参数的method,可以自定义一个method或者考虑给to_proc打个猴子补丁。

参考

《ruby元编程第二版》

Ruby: Tap that method

Understanding Ruby’s idiom: array.map(&:method)