Ruby [译] Five Ruby Methods You Should Be Using

ailen · 2015年01月16日 · 最后由 yumu01 回复于 2015年04月01日 · 7887 次阅读
本帖已被管理员设置为精华贴

1.Object#tap

是否有过当你调用某个对象的方法时候,返回值并不是你想要的?这时候你就需要重新获取对象,并对对象做操作。比如:你想要给存储一系列参数的 hash 加一个任意值,你通过 hash.[] 来更新 hash,因此你必须明确地返回才能得到更新后的结果,代码如下:

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

代码最后一行的 params 看上去似乎是多余的。

因此可以通过Object#tap 来简化代码。对象只要调用 tap 方法,传递一个你需要执行的代码块,对象调用代码块的后返回自己。改进的 update_params 代码如下:

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

Object#tap在很多地方都很有用.有一个简单法则:对象的方法本身不必要返回自己,当你需要时候即可以用Object#tap来替换。

2.Array#bsearch

不知道你们在工作中是否会遇到遍历数组?Ruby 的枚举提供了很多寻找方法比如:select,reject 和 find,这些是我日常工作中经常使用的,但是当数据集很大的时候,我开始担心遍历这些数组需要耗费很多的时间。

如果你使用过 ActiveRecord 来做 SQL 查询,在 ActiveRecord 背后封装了很多魔法,因此你必须要把搜索复杂的降到最低。但是很多时候你必须要把所有数据先查询出来然后才能做进一步工作。比如:当数据库中的数据做了加密的时候,你不能直接通过 SQL 来做操作。

在这个时候,我通常思考如何使用算法复杂度最低 (大 O) 的算法来做数据筛选,如果你不知道大 O 可以参考 Justin Abrahms's 的Big-O Notation Explained By A Self-Taught Programmer或者Big-O Complexity Cheat Sheet

基本的原则就是算法的复杂度决定着算法的运行时间的长短,复杂度从小打到依次排序为:O(1), O(log n), O(n), O(n log(n)), O(n^2), O(2^n), O(n!),因此我们倾向于使用复杂度低的算法。

在 Ruby 中,当对数组做查询的时候,第一反应是使用 Enumerable#find,也可以是 detect。但是这个方法会遍历整个数组,直到条件匹配。如果最终结果在前面那么还好,如果结果在数组最后面,那么最终搜索的复杂度为 O(n)。

在 Ruby 中有一个更快的方法Array#bsearch, 搜索的复杂度为 O(log n),如果想要查看 Binary Search 的工作原理,可以查看Building A Binary Search

下面是从 50,000,000 数字中搜索特定值的运行时间对比:

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 速度非常快。但是使用 bsearch 的大前提就是:数组必须是排序好的。这在很多时候限制了 bsearch 的用途,但是仍然在很多时候会用到,比如:当你在按照 created_at 排序好的数据库记录中按时间查询某个记录。

3.Enumerable#flat_map

当处理相关联的数据时候,我们通常需要查询一些不相关一些列数据,然后以嵌套的数组的形式返回。假设你有一个博客,你想查询出某些用户的上个月的所有的博客的评论的作者,代码如下:

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

你最终有可能得到如下结果:

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

但是真正的需求是我想得到的是作者,而不是 [] 或者 nil,因此你可以通过 flatten 来合并:

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

Ruby中还有一种更好的方法就是使用flat_map:

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

后面两种并没有太大区别,但是使用 flat_map 没必要多次调用 flatten。

4.Array.new with a Block

曾经在我集训的时候,我们导师 Jeff Casimir founder of (Turing School)让我们在一小时时间内去开发一个 Battleship,这是一次很好的面向对象的编程练习,我们需要 Rules, Players, Games, and Boards。创建 Board 的表现形式是一个很有趣的练习,在多次迭代之后,我发现了最简单的方式去构建一个 8x8 的格子:

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

那么到底发生了什么呢?当你调用Array.new(n),就会创建一个长度为 n 的数组:

Array.new(8)
=> [nil, nil, nil, nil, nil, nil, nil, nil]

当你传递代码块的时候,就会把代码块中的值加入到数组中:

Array.new(8) { 'O' }
=> ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O']

因此如果你在代码块中传第一个包含 8 个'0'的数组,那么你就会得到一个 8x8 的所有元素为'0'的数组。使用 Array.new 加上代码块,你可以构建出任何大小,任何嵌套的数组。

5.<=>

这个如"宇宙飞船"的符号是我在 Ruby 中最喜欢的结构之一,它在广泛使用于 Ruby 的内置类中,同时在枚举中也广泛使用。

下面通过整形数值调用<=>来看一下用法,当你运行 5<=>5 时候,返回值 0,当你运行 4<=>5 返回 -1,当你运行 5<=>4,返回 1.因此,如果两个数字一样,返回 0,前者小于后者,返回 -1,如果前者大于后者返回 1.

你可以使用<=>来重新定义自己的逻辑,使其只返回 0,1,-1.

下面是<=>的一个非常 cool 的使用场景,来自 Exercism,有一个练习叫做 Clock,你必须使用自定义的 + 和 - 来调整时钟的小时分钟。当时间超过 60 分钟的时候就会变得非常复杂,因为超过 60 分钟是不合法的数据,因此你必须要加一个小时,同时减去 60 分钟。

一个叫 dalexj 的聪明小伙子的解决方案如下:

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

<=>符号非常适用于定义你自己对对象的排序,同时可以用于某些数学运算,因为它仅仅返回三个固定的数字。

原文链接为:https://blog.engineyard.com/2015/five-ruby-methods-you-should-be-using

处女翻译贴献给了 ruby-china,如果哪里有错误的请大家及时指正。😄

沙发。。。。不错。;。

👍 <=>是 ruby 方法里最神奇的方法之一,很多类关于比较的方法,都会调用这个方法,通过重写这个方法来实现自定义排序和比较。

赞,好快,昨天才看到原文。

markdown 中的链接语法 []()

@appell 谢谢,很久没用了,居然都用反了

宇宙飞船还要小心会出现这种情况

[7] pry(main)> 1 <=> nil
=> nil
[8] pry(main)> nil <=> 1
=> nil
[9] pry(main)> nil <=> nil
=> 0

多来点!后面几个都是读文档的时候了解过,使用经验很少……是不是太高阶了!

@hiveer 对于一个问题有多种解法,每个人的追求不一样,有的人觉得实现就好,有的人觉得一定要找出相对比较好的方案,高不高阶就看你自己定义了。说实话这里面几点我在翻译之前很少用,但是回头过去看以前的代码觉得有必要去 nitpick 一下。

不错,顶得飞起

感觉 tap 方法反而让代码更复杂,不好写也不好读。相较之下我更倾向原来的写法,尽管有点冗余但是非常清楚。

@nagae_memooff 用最熟悉的,而不是最好的

仔细看了下最后一个例子,说实话没有看懂。@ailen 求更多解释

精华哦,恭喜

@tuoxiaozhong 2015 年每周翻译或者出一篇文章,反正要多些文章啊,这样才有提高啊

#16 楼 @ailen 你开始回馈社区了,我们还是观众!

很好,学习了

Array.new(8) { 'O' }
=> ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O']

可以简化

Array.new(8, "O")
=> ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O']

@villins

irb(main):002:0> ['0'] * 8
=> ["0", "0", "0", "0", "0", "0", "0", "0"]

这样岂不是更好么? 翻译时候想到了更简单的方法,但是毕竟是翻译,所以还是保留了原来的.😄

#20 楼 @ailen Array.new后面的 block 是可以传入参数的。比如构建 0 1 4 9 16 25 这样的数组时候,可以直接Array.new(6) {|i|i**2}

<=> 这个的用法太好了,今天才接触到

工作经常碰到,基本能解决大部分问题

需要 登录 后方可回复, 如果你还没有账号请 注册新账号