为了账号安全,请及时绑定邮箱和手机立即绑定

Tap, Inject and Each_with_object in Ruby

标签:
Ruby

Ruby 中 Tap, Inject 和 Each_with_object

中文翻译说明:

  1. 分析 inject 和 Each_with_object 时用到“叠加器”这个词,其代表调用这两个方时,块中的 memo 和 memo_obj 参数

  2. 类枚举类型指的是 Hash, Array 等能够被遍历的数据类型

在工作中使用 Ruby 作为主编程语言两年有余,从刚开始的惊讶到喜欢其优雅,这篇文章总结三个有助于编写可读性强且简洁的代码的方法,重构代码时会经常用到。

Object#tap

tap 方法把当前的对象传给块(block), 并且返回当前调用对象。返回值为当前调用对象的模式能够继续调用对象的公有方法,以此形成了一种链式调用的效果。

查看源码,其实现如下:

class Object
  def tap
    yield self
    self
  endend

在来看看一个实际的例子,假设在 Rails controller 中我们需要对用户传递的参数处理,如果没有使用 tap ,我们可能会写出这样的代码

def update_params(params)
  params[:foo] = "bar"
  paramsend

对一个参数值处理了之后,改方法返回参数哈希。但是现在我们知道 tap 方法会返回对象,所以改造之后的方法如下:

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

改造之后的代码是不是很简洁优雅?不言而明~
另外再来看看充分显示链式调用的例子:

User
  .active                      .tap { |users| puts "Users so far: #{users.size}" }
  .non_admin                   .tap { |users| puts "Users so far: #{users.size}" }
  .at_least_years_old(25)      .tap { |users| puts "Users so far: #{users.size}" }
  .residing_in('USA')

Enumerable#each_with_object #inject methods

枚举类型中这两个方法用的好能极大地优化代码,但同时因为忽略两者的差异写出有 bug 的代码。

inject

通过二元运算对枚举类型(Array, Hash ...)的所有元素操作,可通过块(block),或传符号(symbol) 的参数调用。
【这里的符号可以是方法名或者操作符,例如 :method_name, :+  但请注意,:operator 的方式使用比较多】

每遍历一个元素,就会把块的返回值赋值给 memo,修改了叠加器的值,改方法最后的返回值为最后一次遍历块的返回值,如果想返回叠加器的值,应当在块中返回叠加器(下文有例子讨论)

inject(initial, sym)  obj
inject(sym)  obj
inject(initial) { |memo, obj| block }  obj
inject { |memo, obj| block }  obj

有两种使用方式:传递符号参数调用(symbol)、块调用(block)。

  • 传递符号
    这种使用方式又分两种情况,一种是有初始值(inject(initial, sym) obj),另外一种只有符号(inject(sym) obj)。来看看两个实际例子:

    2.3.1 :003 > (5..10).inject(:+)    # 有初始值=> 452.3.1 :004 > (5..10).inject(1, :+) # 只有符号=> 46
  • 块调用
    块有两个参数,分别是累加器和遍历类枚举类型的值 |accumulator, element|

    (1..5).inject(0){ |sum, num| sum + num } # 15

    初始值为 0 ,数组每次被遍历的元素赋值给 num ,每次遍历过程中累加器都被更新,遍历结束后 inject 方法返回块的返回值。
    讲到这里另外据一个例子说明一个常见的坑,在 [1, 2, 3, 4, 5] 数组中,我们期望小于 5 的元素累加。

    [1, 2, 3, 4, 5].inject {|sum, number| sum += number if number < 5} # nil

    上面这种写法返回 nil ,和我们预期的结果不符。原因是遍历最后一个元素时不满足 number < 5 条件,块返回 nil ,然后整个遍历结束,最后得出来的值也是 nil
    正确的写法应该是

    [1, 2, 3, 4, 5].inject do |sum, number|
      if number < 5
        sum + number  end
      sumend

    每次遍历时,块的返回值应该是 sum ,下次遍历取出来元素值即使不满足判断条件,也不至于返回 nil ,而是累加器 sum 的值。
    另外再现一个常见的业务遇到这个坑的例子,我们期望在多个用户对象组成的数组加工成 { user_a_name: phone, user_b_name: phone } 这样的数据结果。

    # 错误的写法User.all.inject({}) do |memo, user|
      memo[user.name] = user.phoneend

    错误的原因在于,遍历第一个元素结束后,块返回 user.phone 为 String 类型,并且赋值给 memo ,遍历第二个元素时 memo[user.name] 会报错。

    2.2.4 :065 > users.inject({}) do |memo, user|2.2.4 :066 >     puts memo.class2.2.4 :067?>   end
    Hash
    NilClass
    NilClass
    NilClass
    NilClass
     => nil

    正确的方法应该是在块中返回 memo 本身,类型一样为 Hash ,所以就下次遍历时 memo[user.name] 就不会报错。当然这里还有一处可优化的地方,块应该返回:
    memo.merge!(user.name => user.phone)
    正确的写法应该是

    users.inject({}){|memo, user| memo.merge!(user.name => user.phone)|

inject 的返回值是最后遍历枚举类型之后,块返回的值。通常情况下,我们期望最后得到叠加器(在例子中体现在 sum 和 memo),虽然把每次遍历计算出来的叠加器的值放在块的最后也可以做到(在举例时我们在块中返回了 sum 和 memo.merge!(user.name => user.phone))显然这不符合 Ruby 程序的优雅。有没有一种方法直接遍历枚举类型结束后返回叠加器的值而不是块的值呢?
Enumerable#each_with_object 能实现。

each_with_object

each_with_object(obj) { |(*args), memo_obj| ... }  obj
each_with_object(obj)  an_enumerator

each_with_object 的返回值是 obj 原始对象,所以不需像 inject 一样在块中返回 迭代器的值。

users.each_with_object({}) do |user, memo| # note object and memo reversed from #inject
    memo[user.name] = user.emailend

参考阅读



作者:黄文威
链接:https://www.jianshu.com/p/44bde59393e6

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消