使用适用于 Ruby 3.0 的 Ruby 2.7 的默认值重写块参数

寻技术 Ruby编程 2023年07月11日 79

简介:在 Ruby 2.7 中运行的奇怪代码

在维护某个 Rails 项目时,我遇到了这个奇怪的代码。

def sample
  data = {
    a: nil,
    b: { x: , y:  },
    c: { x:  },
    d: { y:  },
  }
  data.map do |k, x: , y: |
    [k, x, y]
  end
end

在 Ruby 2.7 中运行这个 sample 方法会得到以下结果:

# Ruby 2.7
sample
#=> [
#     [:a, 0, 0],
#     [:b, 1, 2],
#     [:c, 1, 0],
#     [:d, 0, 2],
#   ] 

这到底是在做什么?

在这个方法中,下面几行为xy设置了默认值。

data.map do |k, x: , y: |

这会导致该方法表现出以下行为:

  • a: nil,然后 xy 都默认为 0。
  • b: { x: 1, y: 2 }对于x变成1,对于y变成2(不使用默认值)。
  • c: { x: 1 }x 为 1,y 的默认值为 0。
  • d: { y: 2 } 对于x 的默认值为0,对于y 的默认值为2。

就个人而言,我认为“不要编写如此复杂的代码”,但在 Ruby 2.7 之前它工作得很好,所以在那之前它可能一直很好。

遇到的问题:此代码在 Ruby 3.0+ 中不起作用

但是,在 Ruby 2.7 中使用 -W:deprecated 选项运行时,会出现以下警告消息。

warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
warning: The called method is defined here

如果此代码按原样在 Ruby 3.0 中运行,则 xy 都为 0,这与 Ruby 2.7 不同。

# Ruby 3.0だとxもyも全部0になる!
sample
#=> [
#     [:a, 0, 0],
#     [:b, 0, 0],
#     [:c, 0, 0],
#     [:d, 0, 0],
#   ] 

这就是 Ruby 3.0 中引入的“关键字参数分离”的效果。

如果k, x: 0, y: 0这样的描述不是块参数而是方法的形参,可以通过如下方式在hash中添加**来避免这个问题。

# 引用元 https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
def foo(k: )
  p k
end

h = { k:  }

# **を付けるとハッシュオブジェクトをキーワード引数に変換できる
foo(**h) #=> 42

但是,由于这次是块参数,所以不能使用添加**的策略。

解决方案:重写代码以获得预期的结果,即使使用 Ruby 3.0

因此,我决定重写代码如下。

 def sample
   data = {
     a: nil,
     b: { x: , y:  },
     c: { x:  },
     d: { y:  },
   }
-  data.map do |k, x: , y: |
+  data.map do |k, h|
+    x, y = { x: , y:  }.merge(h || {}).values_at(:x, :y)
     [k, x, y]
   end
 end

我能够得到与 Ruby 2.7 相同的结果。

# Ruby 3.0(修正後のコード)
sample
#=> [
#     [:a, 0, 0],
#     [:b, 1, 2],
#     [:c, 1, 0],
#     [:d, 0, 2],
#   ] 

我认为还有其他方法可以编写x, y = { x: 0, y: 0 }.merge ... 部分,但似乎没有像“只需添加**”这样的简单解决方案。

特别感谢

关于这件事,我在 ruby​​-jp Slack 频道上向 Ruby 程序员咨询了各种解决方案。
非常感谢所有咨询过我的 Ruby 程序员!

奖金

这是我写这篇文章的时候为了检查操作而写的测试代码。
当您想自己移动时,请使用它。

require 'minitest/autorun'

class BlockParamsTest < Minitest::Test
  def sample
    data = {
      a: nil,
      b: { x: , y:  },
      c: { x:  },
      d: { y:  },
    }
    # Ruby 2.7
    data.map do |k, x: , y: |
      [k, x, y]
    end
    # Ruby 3.0
    # data.map do |k, h|
    #   x, y = { x: 0, y: 0 }.merge(h || {}).values_at(:x, :y)
    #   [k, x, y]
    # end
  end

  def test_sample
    expected = [
      [:a, , ],
      [:b, , ],
      [:c, , ],
      [:d, , ],
    ]
    assert_equal expected, sample
  end
end

原创声明:本文系作者授权九品源码发表,未经许可,不得转载;

原文地址:https://www.19jp.com/show-308624531.html

关闭

用微信“扫一扫”