【问题标题】:Why does Ruby hash not last method?为什么Ruby hash 不是last 方法?
【发布时间】:2020-07-27 15:34:17
【问题描述】:

我解决了一个问题,它需要获取哈希的最后一个键值对。示例:

hash =  { "aa" => { :count => 1, :width => 333 }, "bb" => { :count => 1, :width => 77 } }

我需要结果

result = [ "bb" => { :count => 1, :width => 77 } ]

哈希有第一种方法

hash.first
=> [ "aa" => { :count => 1, :width => 333 } ]

我想使用last方法,但是出现错误

NoMethodError: undefined method `last' for #<Hash:0x00007f82c75a9988>
from (pry):19:in `__pry__'

这是我的解决方案

result = [hash.stringify_keys.keys[1], hash.stringify_keys.values[1]]

我的问题是为什么 Ruby 哈希没有最后一个方法。

【问题讨论】:

  • 试试hash.to_a.last
  • 更好的实现是[ { hash.keys.last =&gt; hash[hash.keys.last] } ]。可以提高一些效率(不必多次调用#keys),但这适用于任何大小的哈希。
  • 哈希(理论上)是无序的,尽管有时底层实现是有序的。这是无法保证的,因此使用 Hash#last 方法没有意义。
  • @ToddA.Jacobs:在 Ruby 中,它被指定(我认为从 Ruby 1.8 左右开始),以插入顺序维护哈希。
  • @user1934428,将其设为 1.9。

标签: ruby


【解决方案1】:

ruby 哈希的first 方法来自Enumerable。它没有last的方法,所以Hash也没有。

如果我猜的话,Enumerable 出于效率原因没有它。由于Enumerable 只需要each 方法即可工作,因此确定最后一个元素需要完全遍历序列(可能无限长)。

Hash 没有它,因为键值映射在概念上不是有序的数据结构。

【讨论】:

  • Ruby 至少从 1.9 开始就有插入排序哈希,可能更早一点。我确实记得有一次必须实现OrderedHash,因为 Ruby 不支持 Hash 中的顺序概念。它并不是像 Go 的 map 迭代器那样在病态上无序(Go 的 map 迭代器在迭代之前有一个明确的随机选择步骤,所以每个起始键都是不同的)。
  • @AustinZiegler:是的,ruby 中的散列恰好保持插入顺序(至少,现在是这样)。因此,限定词“概念上”
【解决方案2】:

考虑以下哈希和数组:

h = { a: 1, b: 2, c: 3}
a = [:a, :b, :c]

如您所知,像数组一样通过整数索引访问 Hash 是不可能的,而是通过键来访问 Hash,这就是哈希数据结构的强大之处。

通过索引访问,很容易实现像Array#last这样的方法(除了真正的实现并且没有参数),而不需要遍历整个数组:

a[a.size - 1] #=> :c

但是你怎么能用哈希呢?

正如@Sergio Tulentsev 提到的,Enumerable#first 已实现,Hash class includes Enumerable 但没有 Enumerable#last。

所以.first 有效:

h.first(2) #=> [[:a, 1], [:b, 2]]
h.first(1) #=> [[:a, 1]]
h.first #=> [:a, 1]

获取最后一个元素的唯一方法是使用一些变通方法,例如:

h.to_a.last #=> [:c, 3]
[h.keys.last, h.values.last] #=> [:c, 3]

Hash documentation 说:

哈希是一个类似于字典的唯一键及其值的集合。也称为关联数组,它们与数组类似,但在数组使用整数作为索引的情况下,哈希允许您使用任何对象类型。

还有:

哈希按照插入相应键的顺序枚举它们的值。

所以,我没有进行基准测试,但也许这是一种有效的方法?

h.reverse_each.first #=> [:c, 3]

【讨论】:

    【解决方案3】:

    我希望没有方法 Hash#last 因为编码器(尤其是老派编码器)很少使用(从 Ruby 1.9 开始)为哈希维护键插入顺序这一事实,即使他们确实使用了需要最后一个键值对的该属性非常不寻常,但又如此容易满足(例如,h.to_a.last),因此不需要向 Ruby 核心添加方法 Hash#last

    我也能理解Ruby和尚不愿意创建Enumerable方法last。例如,如果self(0..Float::INFINITY) 的范围,它将被转换为枚举器(0..Float::INFINITY).each,从而导致块永远执行。

    假设

    h = { "aa" => { :count => 1, :width => 333 },
          "bb" => { :count => 1, :width => 77 } }
    

    考虑可以提取最后一个键值对的各种方法,可以是:

    ["bb", { :count => 1, :width => 77 }]
    

    { "bb" => { :count => 1, :width => 77 } }
    

    我假设需要一个数组,但如果不是,则数组 a 可以轻松转换为具有单个键值对的哈希:[a].to_h

    我想到了几种可能性:

    #1

    h.to_a.last
      #=> ["bb", {:count=>1, :width=>77}]
    

    这是最简单的。唯一的缺点是它会创建一个临时的散列键值对数组。

    #2

    k = h.keys.last
      #=> "bb"
    [k, h[k]]
      #=> ["bb", {:count=>1, :width=>77}]
    

    这也很简单。唯一的缺点是它创建了一个哈希键的临时数组。但是,该临时数组小于 #1 创建的临时数组。

    #3

    n = h.size
      #=> 2
    h.reject { (n -= 1) > 0 }.first
      #=> ["bb", {:count=>1, :width=>77}]
    

    这样做的好处是,虽然它创建了一个包含单个键的临时哈希,但该哈希可能比 #1 和 #2 创建的临时数组消耗更少的内存。见Hash#reject。请注意,如果希望返回值是哈希值,可以写为h.reject { (n -= 1) &gt; 0 } #=&gt; {"bb"=&gt;{:count=&gt;1, :width=&gt;77}}

    #4

    enum = h.to_enum
      #=> #<Enumerator: {"aa"=>{:count=>1, :width=>333},
      #                  "bb"=>{:count=>1, :width=>77}}:each> 
    last = nil
    loop { last = enum.next }
    last
      #=> ["bb", {:count=>1, :width=>77}]
    

    这是迄今为止唯一一种无需构造临时数组或散列即可计算所需数组的方法。请注意,在enum 生成最后一个元素之后,enum.next 会生成一个StopIteration 异常,该异常由Kernel#loop 通过中断循环来处理。

    现在让我们考虑如何创建方法Enumerable#last(认识到我在第二段中提到的陷阱)。

    module Enumerable
      def last
        obj = nil
        each { |o| obj = o }
        obj
      end
    end
    h.last
      #=> ["bb", {:count=>1, :width=>77}]
    

    last 被哈希调用时,第一步(与所有 Enumerable 方法一样)是将其接收者转换为枚举器),通过向其发送方法 each,此处为 Hash#each 1

    1.如果last 的接收者已经是一个枚举器,则Enumerator#each 被发送到接收者,它只针对self 执行块。

    【讨论】:

    • @Stefan 在对该问题的评论中提出了#1。但是,我不能有意识地说,如果他没有这样做,我不会想到这一点。
    • 你的最后一个例子可以改写为def last ; obj = nil ; each { |o| obj = o } ; obj ; end
    • 好建议,@Stefan。它澄清了我对某一点的思考。我原以为Enumerable 方法将each 发送给它的接收者,除非接收者是一个枚举器。我现在认为each 也可能发送给枚举器,因为在枚举器上执行Enumerator#each 可能与检查接收器是否是枚举器一样快。你知道是不是这样吗?
    • 我没有检查实现,但是Enumerable 无论如何调用each 似乎要简单得多。
    猜你喜欢
    • 1970-01-01
    • 2020-01-04
    • 2012-05-12
    • 1970-01-01
    • 2014-08-24
    • 1970-01-01
    • 2021-10-28
    • 2014-09-25
    • 2016-06-23
    相关资源
    最近更新 更多