Ruby数组中容易忽视的Suboptimal代码

首先解释下什么是Suboptimal,根据翻译来看就是未达标准的,非最优的。 本文主要介绍下一些数组中比较细节的但很容易被开发人员忽视的非最优使用方法。

1.寻找Array中满足条件的第一个值

很多人可能会使用以下方法:

1
  [1,2,3].select {|x| x > 1 }.first

但其实有更好的有效率的替代方法:

1
  [1,2,3].detect {|x| x > 1 }

为什么说detect方法更有效率呢,因为select内部会遍历所有的元素,而且会生成新的对象, 而detect方法会在找到第一个满足的值就直接返回退出了。

2.使用reverse循环的时候应该使用reverse_each方法

很多人会使用如下方法:

1
[1,2,3].reverse.each {|x| x+1 }

更好的替代方法如下:

1
[1,2,3].reverse_each {|x| x+1 }

因为先reverse再做each的话会做2遍循环,而reverse_each内部只需做一次循环。

3. 寻找Array中满足条件的是否存在

很多人会这么写:

1
[1,2,3].select {|x| x > 1 }.empty?

更好的替代方法如下:

1
[1,2,3].none? {|x| x > 1 }

原因和第一个一样,select会遍历所有,而none?会遇到满足第一个条件的而停下来返回false。

4. 不要使用连续的select查询用一个select代替

有人会这么写:

1
[1,2,3,4].select {|x| x > 1 }.select {|x| x < 4 }

但最好写成一个select就可以了

1
[1,2,3,4].select {|x| x > 1 && x < 4 }

这个原因很明显就不解释了,感觉犯这种错误的人应该也不多。

5. 计算数组满足条件的值的长度

很多人会这么写:

1
[1,2,3].select {|x| x > 1 }.size

下面这种写法效率会更高点:

1
[1,2,3].count {|x| x > 1 }

select这种写法会生成新的对象,而下面count不会。

6. map后面做flatten操作

很多人会这么写:

1
[1,2,3].map {|x| [x,x+1] }.flatten

更有效率的写法如下:

1
[1,2,3].flat_map {|x| [x, x+1]}

原因是上面做的循环次数比下面方法多。

详解 Ruby 2.0 新特性 Keyword Argument

Ruby 2.0.0推出了一个新的特性Keyword Argument,中文名可能叫关键字参数或者命名参数,我在这里详细介绍下这个特性的具体细节。

什么是Keyword Argument呢?

在这里通过一个例子说明:

1
2
3
4
def log(msg, level: "ERROR", time: Time.now)
  puts "#{ time.ctime } [#{ level }] #{ msg }"
end
log("Hello!", level: "INFO")  #=> Mon Feb 18 01:46:22 2013 [INFO] Hello!

看起来很普通好像没有什么特别新的,是不是?

详细解释

在Ruby 1.9里面也有下面的调用方法方式:

1
log("Hello!", level: "INFO")

上面传的第二个参数是hash,定义像下面这样:

1
2
3
4
5
def log(msg, opt = {})
  level = opt[:level] || "ERROR"
  time  = opt[:time]  || Time.now
  puts "#{ time.ctime } [#{ level }] #{ msg }"
end

但是也许需求不是那么简单:

  • 希望hash如果传给我们不期望的key,需要抛出异常
  • 希望hash的其中一个key可以传nil

那我们需要把方法改成如下:

1
2
3
4
5
6
7
def log(*msgs)
  opt = msgs.last.is_a?(Hash) ? msgs.pop : {}
  level = opt.key?(:level) ? opt.delete(:level) : "ERROR"
  time  = opt.key?(:time ) ? opt.delete(:time ) : Time.now
  raise "unknown keyword: #{ opt.keys.first }" if !opt.empty?
  msgs.each {|msg| puts "#{ time.ctime } [#{ level }] #{ msg }" }
end

如果你处理类似需求的方法,会不会觉得很麻烦。如果使用Ruby 2.0 的新功能Keyword arguments,代码将会变得下面这么简单干净:

1
2
3
def log(msg, level: "ERROR", time: Time.now)
  puts "#{ time.ctime } [#{ level }] #{ msg }"
end

更多细节

我们可以调用的时候,参数部分省略

1
2
log("Hello!")                                  #=> Mon Feb 18 01:46:22 2013 [ERROR] Hello!
log("Hello!", level: "ERROR", time: Time.now)  #=> Mon Feb 18 01:46:22 2013 [ERROR] Hello!

Keyword argument的顺序不是很重要,但其他参数的顺序是不可以随便写的。

1
2
log("Hello!", time: Time.now, level: "ERROR")  #=> Mon Feb 18 01:46:22 2013 [ERROR] Hello!
log(level: "ERROR", time: Time.now, "Hello!")  # 运行错误

但如果你传入一个非期待的key时,将会报错

1
log("Hello!", date: Time.new)  #=> unknown keyword: date

如果你不想这个非期待的key报错,需要加入一个**开头的参数来保存这个key,

1
2
3
4
5
def log(msg, level: "ERROR", time: Time.now, **kwrest)
  puts "#{ time.ctime } [#{ level }] #{ msg }"
end

log("Hello!", date: Time.now)  #=> Mon Feb 18 01:46:22 2013 [ERROR] Hello!

当然你可以将Keyword argument和可变参数混合放一起使用,不太建议这么使用,会减少代码的可读性,增加了复杂度,容易出现bug。

1
2
3
def f(a, b, c, m = 1, n = 1, *rest, x, y, z, k: 1, **kwrest, &blk)
...
end

限制

可变参数和Keyword arguments混用的时候,容易出问题。例子:

1
2
3
4
5
6
7
def foo(*args, k: 1)
  p args
end

args = [{}, {}, {}]

foo(*args) #=> [{}, {}]

可变参数的最后一个值被赋给keyword arguments了

其次你也不能把**作为参数

1
2
3
def foo(**)
end
foo(k: 1) #=> unknown keyword: k

还有参数的key不能ruby里面的保留关键字,例:

1
2
3
def foo(if: false)
end
foo(if: true)

如果想使用if作为key,只能使用**参数如下:

1
2
3
4
def foo(**kwrest)
  p kwrest[:if]
end
foo(if: true) #=> true

解决 Can't Verify CSRF Token Authenticity 错误

问题现象

今天偶尔看看一个项目的代码,然后想运行看看,通过localhost:3000访问,登陆的时候一直报下面的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Started POST "/users/sign_in" for 127.0.0.1 at 2014-11-13 23:11:54 +0800
Processing by Devise::SessionsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"4HS675PeZDP+xBZkb45AJxQouoywke3lAJOvA3IFLt8=", "user"=>{"login"=>"xxx@test.com", "password"=>"[FILTERED]", "remember_me"=>"0"}, "
commit"=>"login"}

Can't verify CSRF token authenticity
Completed 422 Unprocessable Entity in 100ms

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
  actionpack (4.1.1) lib/action_controller/metal/request_forgery_protection.rb:176:in `handle_unverified_request'
  actionpack (4.1.1) lib/action_controller/metal/request_forgery_protection.rb:202:in `handle_unverified_request'
  devise (3.2.4) lib/devise/controllers/helpers.rb:182:in `handle_unverified_request'
  actionpack (4.1.1) lib/action_controller/metal/request_forgery_protection.rb:197:in `verify_authenticity_token'
  activesupport (4.1.1) lib/active_support/callbacks.rb:424:in `block in make_lambda'
  activesupport (4.1.1) lib/active_support/callbacks.rb:160:in `call'
  activesupport (4.1.1) lib/active_support/callbacks.rb:160:in `block in halting'
  activesupport (4.1.1) lib/active_support/callbacks.rb:166:in `call'
  activesupport (4.1.1) lib/active_support/callbacks.rb:166:in `block in halting'
  activesupport (4.1.1) lib/active_support/callbacks.rb:149:in `call'
  activesupport (4.1.1) lib/active_support/callbacks.rb:149:in `block in halting_and_conditional'
  activesupport (4.1.1) lib/active_support/callbacks.rb:149:in `call'
  activesupport (4.1.1) lib/active_support/callbacks.rb:149:in `block in halting_and_conditional'
  activesupport (4.1.1) lib/active_support/callbacks.rb:149:in `call'
  activesupport (4.1.1) lib/active_support/callbacks.rb:149:in `block in halting_and_conditional'
  activesupport (4.1.1) lib/active_support/callbacks.rb:86:in `call'
  activesupport (4.1.1) lib/active_support/callbacks.rb:86:in `run_callbacks'
  actionpack (4.1.1) lib/abstract_controller/callbacks.rb:19:in `process_action'
  actionpack (4.1.1) lib/action_controller/metal/rescue.rb:29:in `process_action'

原因推测

google 搜了下,很多人是ajax提交登陆的时候,没有设置authenticity_token才会发生, 但我这个是有authenticity_token的。

解决方法

最后发现原来此项目的session_store.rb配置了特定的域名,应该是为了子域名session共享而配置的。

1
2
3
4
Lvh::Application.config.session_store :cookie_store, key: '_lvh_session', domain: {
  production: '.lvh.com',
  development: '.lvh.local'
}.fetch(Rails.env.to_sym, :all)

所以本地开发环境访问的时候必须使用 lvh.local:3000访问, 这样登陆就没有问题了。

我的第一个全程马拉松

11月2号早上5点伴随着闹钟,早早的爬起床来,已经好久没有这么早起床了,还真有点不习惯。 快速洗漱过后,去赶公交,在公交上也看到了好几位参加2014上海国际马拉松的选手,有的人竟然只穿着短裤,还是女的,真是太厉害了。 从南京东路地铁口,发现好多参加比赛的选手,不是一般般多,看来花钱找罪受的人还不少,呵呵我也是其中一个。 本来需要等同事一起去比赛现场,可惜他打不到车,只能独自先去找换衣车。一路上发现人超多,上厕所的都排了好长的几条队伍。 到了比赛准备区,很多人在热身,好吧。我作为第一次参加的菜鸟也混了进去,进行了貌似很专业的热身动作,随着比赛时间越来越近, 什么样的人都来了,西游记的师徒四人,蓝精灵,济公。。。这些人就是来赚眼球的。

比赛开始,随着发令枪响起,我等了2分钟才出发,前面的人实在太多,刚起跑不久,下起了大雨,还很冷。。。真是郁闷。第一次跑马拉松就 遇到这种鬼天气,一开始大家跑的都很快,我也不急,重在参与,计划跑个20多公里退出,也不丢脸。所以省点体力,不要跑不到一半就退出,就 有点丢脸了。伴随着路边大妈和美女的加油声,不知不觉就跑完了10公里,大概花了1个多小时,这时雨也差不多停了,路上补充了一次水分。感觉 不是很累,我想20公里看来可以坚持下来。跑了大概18公里的时候,明显感觉有点累了,速度也慢了下来,这时也感觉有点饿了,路边只有饮料可以 补充,自己又忘记带能量补充,没办法只能多喝点饮料补充了。就这样坚持到了23公里,发现自己好像还能继续跑,只是跑一会后,稍微走一会也能 撑下去,想想继续跑吧,能坚持跑到30公里也算突破自己很多了,我平时练习最多才10公里。跑到快30公里的时候,发现隔壁的马路上写着39公里, 很多人在叫还有3公里了,加油啊!!!我想,靠,这些什么人啊咋这么快,我才30公里呢。那时候我已经开始每公里80%靠跑,20%靠走的频率再坚持, 跑到35公里开始感觉脚和关节已经很痛了,跑步和走路的分配基本就2:8了。这时不远的收容车在向你招手,自己想想都35公里了放弃太可惜了,继续坚持吧。就这样竟然坚持到了37公里,这时已经过去了4个小时,这时2个腿已经完全太不起来了,实在很痛,感觉脚底皮都磨破了,也有抽筋的迹象。 很多人都已经在步行了,我想算了就步行吧,反正6个小时,估计也能走到终点。就这样走走停停,路上再喝点饮料补充补充。拖着双腿,终于抵达终点,终点本想跑过去,最后发现还是只能走过去了,还好在6个小时内完赛了。

真的不可思议,第一次比赛,平时最多练过10公里,竟然跑完了全程。看来很多事情,不尝试一下,你永远都不知道自己的潜力有多大,希望下次有机会再次参加全程马拉松,争取跑出更好的成绩。

关于上海马拉松的组织:

总体组织的不错,包括后勤,补给站的设置,马路的监管,存衣库的管理。

我的个人建议:厕所貌似还是有点少,如果路边还有些食物补给就更好了。

避免mongoid循环处理太久时间

问题现象

使用mongoid的过程中发现一个问题,一个很简单的数据库查询,然后进行循环处理,循环始终跑不到最后,会抛出Moped::Errors::CursorNotFound异常。

1
2
3
  User.where(...).each do |user|
    # do something long time
  end

问题原因

官方说是查询时间太久了,导致cursor被服务器kill掉了

解决方法

第一种方法

将查询出来的结果放到临时变量再循环

1
2
3
4
  users = User.where(...)
  users.each do |user|
    # do something long time
  end

第二种方法

no_timeout方法再循环

1
2
3
  User.where(...).no_timeout.each do |user|
    # do something long time
  end

Rails 4.2性能提升新特性Adequate Record介绍

最近看了Rails 4.2的更新bolg, 发现在Rails 4.2中加了个Adequate Record的新特性,这个特性是用来改善数据库查询性能的,看了介绍, 把我所理解的写了下来。

性能提升变化图

性能变化图

测试的是Model.find(id)Model.find_by_name(name)方法, 提升了将近2倍,真厉害啊!

性能提升方法

一般的ActiveRecord查询转换成SQL数据库查询包含下面3个步骤

查询步骤

而最新的rails一般find查询都会被转化为如下SQL

1
2
3
4
SELECT * FROM posts WHERE id = ? [id, 10]
SELECT * FROM posts WHERE id = ? [id, 12]
SELECT * FROM posts WHERE id = ? [id, 22]
SELECT * FROM posts WHERE id = ? [id, 33]

你可以注意到SQL查询语句都是一样的,只是参数不一样。所以可以将3个步骤的第一步缓存起来, 如果下次做类似find查询,就可以跳过第一步,这样性能就提升了。

但性能提升只限下面3个方法:

1
2
3
Post.find(id)
Post.find_by_name(name)
Post.find_by(name: name)

参考链接

http://tenderlovemaking.com/2014/02/19/adequaterecord-pro-like-activerecord.html

Rails里面改变validate错误信息默认的属性名字

例如有如下代码

1
2
3
class User < ActiveRecord::Base
  validates :name, :presence => true
end

默认的错误信息将会为

1
Name can't be blank

如果想改变错误信息为 “User Name can’t be blank”怎么实现呢?

1
2
3
4
5
6
7
8
9
10
11
12
class User < ActiveRecord::Base
  validates :name, :presence => true

  HUMANIZED_ATTRIBUTES = {
    :name => "User Name"
  }

  def self.human_attribute_name(attr, options={})
    HUMANIZED_ATTRIBUTES[attr.to_sym] || super
  end

end