原文: Five Ruby Methods You Should Be Using - Ben Lewis

Ruby有很多神奇的语法可以使代码更简洁和易读,下面看看几个鲜为人知的解决特定问题的方法:

1. Object#tap

在调用某个对象的方法时,返回值并不是我们想要的,我们希望得到对象本身,比如实现一个方法为一个hash对象添加键值,但结果会得到「值」而不是hash对象本身,所以我们需要在最后明确地返回它。

def update_params(params)
  params[:foo] = 'bar'
  params
end

这时候最后一行的params就可以用Object#tap来清理掉它。只需要在调用tap时,将代码放到它的block块中,这个对象将会被占位(yield)执行然后被返回。

def update_params(params)
  params.tap {|p| p[:foo] = 'bar' }
end

2. Array#bsearch

平时写业务代码时,Enumerable对象的select、reject、find这些方便的方法常会被用到,但当数据量很大时,这些方法就可能会花费更多时间。

如果我们使用ActiveRecord处理SQL数据库时,背后会发生很多魔术转换来确保我们的搜索以最少的算法复杂性进行。 但有时候我们不得不取出所有数据然后进行查找提取需要的数据。

这时候基于算法的复杂度就会决定提取数据需要花费的时间,这个算法可以用Big-O分类,依次为: O(1), O(log n), O(n), O(n log(n)), O(n^2), O(2^n), O(n!)

首先想到的是使用Enumerable#find,也称为detect,但这个方法会搜索整个列表直到找到匹配,如果这个记录在列表的开头那还好,但如果是在一个数据量很大的列表的结尾,就会出现问题,这需要O(n)的复杂度。

还好有个更快的方法Array#bsearch,它查以查找复杂度为O(log n)的匹配(关于二进制搜索)。

下面是在搜索5千万个数字时,两个方法的搜索时间的差异:

require 'benchmark'

data = (0..50_000_000)

Benchmark.bm do |x|
  x.report(:find) { data.find {|number| number > 40_000_000 } }
  x.report(:bsearch) { data.bsearch {|number| number > 40_000_000 } }
end

         user       system     total       real
find     3.020000   0.010000   3.030000   (3.028417)
bsearch  0.000000   0.000000   0.000000   (0.000006)

看起来bsearch要快得多,但前提是数组必须是被排序的,这显然限制了它的使用场景,但对于从数据库取出的已经经过排序的数据集(比如按created_at排序)来说,还是值得记住的。

3. Enumerable#flat_map

处理关系数据时,有时候需要收集一些相关的属性,并将他们返回到一个「扁平」的数组中。 比如需要为一个博客程序返回指定用户上个月撰写的博文下所有评论的作者名。 我们可能会这么做:

module CommentFinder
  def self.find_for_users(user_ids)
    users = User.where(id: user_ids)
    users.map do |user|
      user.posts.map do |post|
        post.comments.map |comment|
          comment.author.username
        end
      end
    end
  end
end

最后得到的结果应该是这样:

[[['Ben', 'Sam', 'David'], ['Keith']], [[], [nil]], [['Chris'], []]]

但我们需要数组是「扁平」的,所以需要使用flatten方法:

module CommentFinder
  def self.find_for_users(user_ids)
    users = User.where(id: user_ids)
    users.map { |user|
      user.posts.map { |post|
        post.comments.map { |comment|
          comment.author.username
        }.flatten
      }.flatten
    }.flatten
  end
end

当然还可以使用flat_map:

module CommentFinder
  def self.find_for_users(user_ids)
    users = User.where(id: user_ids)
    users.flat_map { |user|
      user.posts.flat_map { |post|
        post.comments.flat_map { |comment|
          comment.author.username
        }
      }
    }
  end
end

4. Array.new with a Block

比如我们需要为一个小游戏创建一个8x8的含用指定字符的地板网络,可以方便地使用block完成:

class Board
  def board
    @board ||= Array.new(8) { Array.new(8) { '0' } }
  end
end

这样做使代码简洁了很多,在不知道这个方法之前,通过我们创建指定长计的数组时它的所有元素都会是nil。 指定block后,通过block可以为每个元素填充默认内容。

5. <=>

排序运算符被内置在Ruby大多数类中,在使用枚举类型时很有用。

为了说明它原理,可以试着来看看fixnums的行为。如果调用5<=>5,则返回0,如果调用4<=>5则返回-1,如果调用5<=>4则返回1。基本上如果两个数字相同则返回0,正序排列则返回-1,倒序排列则返回1

我们可以在自己的类中实现排序运算符,包括比较模块,并重新定义<=>的逻辑,返回-1, 0, 1

下面尝试在一个根据分钟数计算时间的练习中使用它:

def fix_minutes
  until (0...60).member? minutes
    @hours -= 60 <=> minutes
    @minutes += 60 * (60 <=> minutes)
  end
  @hours %= 24
  self
end

它的原理是: 基于过去的分钟数(小于60或大于60)减于1-1的小时数,然后调整分钟数,加上-6060

排序运算符很适合为自己的对象自定义排序顺序,也可以利用返回的三个值做算术运算。

相关