14

概括

给定一个哈希,根据要使用的键列表创建子集哈希的最有效方法是什么?

h1 = { a:1, b:2, c:3 }        # Given a hash...
p foo( h1, :a, :c, :d )       # ...create a method that...
#=> { :a=>1, :c=>3, :d=>nil } # ...returns specified keys...
#=> { :a=>1, :c=>3 }          # ...or perhaps only keys that exist

细节

Sequel数据库工具包允许通过传入 Hash 来创建或更新模型实例:

foo = Product.create( hash_of_column_values )
foo.update( another_hash )

Sinatra Web 框架提供了一个名为 Hash 的名称,params其中包括表单变量、查询字符串参数以及路由匹配。

如果我创建一个仅包含与数据库列名称相同的字段并将其发布到此路由的表单,那么一切都非常方便:

post "/create_product" do
  new_product = Product.create params
  redirect "/product/#{new_product.id}"
end

然而,这既脆弱又危险。这很危险,因为恶意黑客可能会发布一个包含不打算更改的列的表单并让它们更新。它很脆弱,因为在这条路线上使用相同的表格是行不通的:

post "/update_product/:foo" do |prod_id|
  if product = Product[prod_id]
    product.update(params)
    #=> <Sequel::Error: method foo= doesn't exist or access is restricted to it>
  end
end

因此,为了稳健性和安全性,我希望能够编写以下代码:

post "/update_product/:foo" do |prod_id|
  if product = Product[prod_id]
    # Only update two specific fields
    product.update(params.slice(:name,:description))
    # The above assumes a Hash (or Sinatra params) monkeypatch
    # I will also accept standalone helper methods that perform the same
  end
end

...而不是更冗长和非 DRY 选项:

post "/update_product/:foo" do |prod_id|
  if product = Product[prod_id]
    # Only update two specific fields
    product.update({
      name:params[:name],
      description:params[:description]
    })
  end
end

更新:基准

以下是对(当前)实现进行基准测试的结果:

                    user     system      total        real
sawa2           0.250000   0.000000   0.250000 (  0.269027)
phrogz2         0.280000   0.000000   0.280000 (  0.275027)
sawa1           0.297000   0.000000   0.297000 (  0.293029)
phrogz3         0.296000   0.000000   0.296000 (  0.307031)
phrogz1         0.328000   0.000000   0.328000 (  0.319032)
activesupport   0.639000   0.000000   0.639000 (  0.657066)
mladen          1.716000   0.000000   1.716000 (  1.725172)

@sawa 的第二个答案是最快的,在我tap的基于实现的前面有一根头发(基于他的第一个答案)。选择添加检查只需要has_key?很少的时间,而且速度仍然是 ActiveSupport 的两倍多。

这是基准代码:

h1 = Hash[ ('a'..'z').zip(1..26) ]
keys = %w[a z c d g A x]
n = 60000

require 'benchmark'
Benchmark.bmbm do |x|
  %w[ sawa2 phrogz2 sawa1 phrogz3 phrogz1 activesupport mladen ].each do |m|
    x.report(m){ n.times{ h1.send(m,*keys) } }
  end
end
4

5 回答 5

19

我只会使用 active_support 提供的 slice 方法

require 'active_support/core_ext/hash/slice'
{a: 1, b: 2, c: 3}.slice(:a, :c)                  # => {a: 1, c: 3}

当然,请确保更新您的 gemfile:

gem 'active_support'
于 2011-04-13T19:27:56.863 回答
5

我改变了主意。上一个好像不太好。

class Hash
  def slice1(*keys)
    keys.each_with_object({}){|k, h| h[k] = self[k]}
  end
  def slice2(*keys)
    h = {}
    keys.each{|k| h[k] = self[k]}
    h
  end
end
于 2011-04-13T17:35:52.323 回答
3

Sequel 内置支持仅在更新时选择特定列:

product.update_fields(params, [:name, :description])

但是,如果 :name 或 :description 不存在于 params 中,那将不会做同样的事情。但是假设您希望用户使用您的表单,那应该不是问题。

我总是可以扩展 update_fields 以获取带有选项的选项散列,如果散列中不存在该值,则该选项将跳过该值。我只是还没有收到这样做的请求。

于 2011-04-14T19:41:13.943 回答
2

也许

class Hash
  def slice *keys
    select{|k| keys.member?(k)}
  end
end

或者你可以复制 ActiveSupport 的Hash#slice,它看起来更健壮一些。

于 2011-04-13T17:50:37.550 回答
0

这是我的实现;我将基准测试并接受更快(或更优雅)的解决方案:

# Implementation 1
class Hash
  def slice(*keys)
    Hash[keys.zip(values_at *keys)]
  end
end

# Implementation 2
class Hash
  def slice(*keys)
    {}.tap{ |h| keys.each{ |k| h[k]=self[k] } }
  end
end

# Implementation 3 - silently ignore keys not in the original
class Hash
  def slice(*keys)
    {}.tap{ |h| keys.each{ |k| h[k]=self[k] if has_key?(k) } }
  end
end
于 2011-04-13T17:11:49.017 回答