有没有理由我们不能在 ruby​​ 中迭代“反向范围”?

Posted

技术标签:

【中文标题】有没有理由我们不能在 ruby​​ 中迭代“反向范围”?【英文标题】:Is there a reason that we cannot iterate on "reverse Range" in ruby? 【发布时间】:2011-01-05 10:45:35 【问题描述】:

我尝试使用 Range 和 each 向后迭代:

(4..0).each do |i|
  puts i
end
==> 4..0

遍历0..4 写入数字。在另一个范围r = 4..0 似乎没问题,r.first == 4r.last == 0

上面的构造并没有产生预期的结果,这对我来说似乎很奇怪。这是什么原因?这种行为在什么情况下是合理的?

【问题讨论】:

我不仅对如何实现这个迭代感兴趣,这显然是不支持的,而是它为什么返回范围 4..0 本身。语言设计者的意图是什么?为什么,在什么情况下是好的?我在其他 ruby​​ 构造中也看到了类似的行为,但它在有用时仍然不干净。 范围本身按约定返回。由于.each 语句没有修改任何内容,因此没有计算出的“结果”要返回。在这种情况下,Ruby 通常会在成功时返回原始对象,在错误时返回 nil。这使您可以将这样的表达式用作if 语句的条件。 【参考方案1】:

范围就是这样:由它的开始和结束定义的东西,而不是它的内容。在一般情况下,在一个范围内“迭代”并没有真正的意义。例如,考虑如何“迭代”两个日期产生的范围。你会每天迭代吗?按月?按年?按周?它没有很好的定义。 IMO,允许向前范围的事实应仅被视为一种方便的方法。

如果你想在这样的范围内向后迭代,你总是可以使用downto

$ r = 10..6
=> 10..6

$ (r.first).downto(r.last).each  |i| puts i 
10
9
8
7
6

这里有来自其他人的some more thoughts,讲述了为什么既要允许迭代又要始终如一地处理反向范围很困难。

【讨论】:

我认为迭代从 1 到 100 或从 100 到 1 的范围直观地意味着使用步骤 1。如果有人想要不同的步骤,请更改默认值。同样,对我来说(至少)从 1 月 1 日到 8 月 16 日迭代意味着按天计算。我认为我们通常可以达成共识,因为我们直觉上就是这么说的。感谢您的回答,您提供的链接也很有用。 我仍然认为为许多范围定义“直观”迭代很难始终如一地进行,而且我不同意以这种方式迭代日期直观地意味着等于 1 天的步骤——毕竟,一天本身已经是一个时间范围(从午夜到午夜)。例如,谁能说“1 月 1 日至 8 月 18 日”(正好 20 周)并不意味着迭代数周而不是数天?为什么不按小时、分钟或秒进行迭代? .each 有多余的,5.downto(1) |n| puts n 工作正常。此外,不要使用所有 r.first r.last 的东西,只需执行 (6..10).reverse_each @Mk12: 100% 同意,我只是为了新的 Ruby 主义者而试图变得超级明确。不过,也许这太令人困惑了。 当尝试在表单中添加年份时,我使用了:= f.select :model_year, (Time.zone.now.year + 1).downto(Time.zone.now.year - 100).to_a【参考方案2】:

(0..1).reverse_each 向后迭代范围怎么样?

【讨论】:

它构建临时数组,至少在 2.4.1 ruby-doc.org/core-2.4.1/Enumerable.html#method-i-reverse_each 这对我对一系列日期进行排序(Date.today.beginning_of_year..Date.today.yesterday).reverse_each谢谢【参考方案3】:

在 Ruby 中使用 each 迭代一个范围会在该范围内的第一个对象上调用 succ 方法。

$ 4.succ
=> 5

5 超出范围。

您可以使用此 hack 模拟反向迭代:

(-4..0).each  |n| puts n.abs 

John 指出,如果它跨越 0,这将不起作用。这将:

>> (-2..2).each  |n| puts -n 
2
1
0
-1
-2
=> -2..2

不能说我真的很喜欢他们中的任何一个,因为他们有点模糊了意图。

【讨论】:

不可以,但可以乘以 -1 而不是使用 .abs。【参考方案4】:

另一种方式是(1..10).to_a.reverse

【讨论】:

【参考方案5】:

根据《Programming Ruby》一书,Range 对象存储范围的两个端点,并使用.succ 成员生成中间值。根据您在范围内使用的数据类型,您始终可以创建 Integer 的子类并重新定义 .succ 成员,使其像反向迭代器一样(您可能还想重新也定义.next)。

您也可以在不使用 Range 的情况下实现您正在寻找的结果。试试这个:

4.step(0, -1) do |i|
    puts i
end

这将以 -1 的步长从 4 步进到 0。但是,我不知道这是否适用于除整数参数之外的任何东西。

【讨论】:

【参考方案6】:

您甚至可以使用for 循环:

for n in 4.downto(0) do
  print n
end

哪个打印:

4
3
2
1
0

【讨论】:

【参考方案7】:

如果列表不是那么大。 我想 [*0..4].reverse.each |i| puts i 是最简单的方法。

【讨论】:

IMO 通常认为它很大是好的。我认为这是普遍遵循的正确信念和习惯。由于魔鬼从不睡觉,我不相信自己记得我在哪里迭代了一个数组。但你是对的,如果我们有 0 和 4 常量,遍历数组可能不会导致任何问题。【参考方案8】:

正如 bta 所说,原因是Range#eachsucc 发送到它的开头,然后发送到succ 调用的结果,依此类推,直到结果大于结束值。调用succ 无法从 4 到 0,事实上,你的开始已经大于结束。

【讨论】:

【参考方案9】:

我添加了另一种可能性,即如何实现反向 Range 上的迭代。我不使用它,但这是一种可能性。猴子补丁红宝石核心对象有点冒险。

class Range

  def each(&block)
    direction = (first<=last ? 1 : -1)
    i = first
    not_reached_the_end = if first<=last
                            lambda |i| i<=last
                          else
                            lambda |i| i>=last
                          end
    while not_reached_the_end.call(i)
      yield i
      i += direction
    end
  end
end

【讨论】:

【参考方案10】:

OP 写的

对我来说,上面的构造没有产生似乎很奇怪 预期的结果。这是什么原因?什么是 这种行为在什么情况下是合理的?

不是“可以做到吗?”但要在回答实际提出的问题之前回答未提出的问题:

$ irb
2.1.5 :001 > (0..4)
 => 0..4
2.1.5 :002 > (0..4).each  |i| puts i 
0
1
2
3
4
 => 0..4
2.1.5 :003 > (4..0).each  |i| puts i 
 => 4..0
2.1.5 :007 > (0..4).reverse_each  |i| puts i 
4
3
2
1
0
 => 0..4
2.1.5 :009 > 4.downto(0).each  |i| puts i 
4
3
2
1
0
 => 4

既然 reverse_each 声称要构建一个完整的数组,那么 downto 显然会更有效率。语言设计者甚至可以考虑实现类似的东西这一事实与实际问题的答案有关。

按照实际问题回答问题...

原因是因为 Ruby 是一种无穷无尽的语言。有些惊喜是令人愉快的,但有很多行为是彻头彻尾的坏事。即使以下示例中的一些已被较新版本纠正,还有很多其他示例,它们仍然是对原始设计思维方式的控诉:

nil.to_s
   .to_s
   .inspect

导致 "" 但是

nil.to_s
#  .to_s   # Don't want this one for now
   .inspect

结果

 syntax error, unexpected '.', expecting end-of-input
 .inspect
 ^

你可能会期望

a = []
a << *[:A, :B]    # is illegal but
a.push *[:A, :B]  # isn't.

您可能希望 'grep' 的行为与其 Unix 命令行等效项一样,但它确实 === 匹配而不是 =~,尽管它的名称。

$ echo foo | grep .
foo
$ ruby -le 'p ["foo"].grep(".")'
[]

各种方法出人意料地互为别名,因此您必须为同一事物学习多个名称 - 例如。 finddetect - 即使您确实喜欢大多数开发人员并且只使用其中一个。 sizecountlength 的情况大致相同,除了定义不同的类,或者根本不定义一两个。

除非有人实现了其他东西——比如核心方法tap 已在各种自动化库中重新定义,以在屏幕上按下某些东西。祝你好运,找出发生了什么,特别是如果某个其他模块所需的某个模块已经欺骗另一个模块来做一些未记录的事情。

环境变量对象,ENV不支持'merge',所以要自己写

 ENV.to_h.merge('a': '1')

作为奖励,如果您改变主意,您甚至可以重新定义您或其他人的常量。

【讨论】:

这不会以任何方式、形状或形式回答问题。这只不过是对作者不喜欢 Ruby 的一些事情的咆哮。 已更新以回答未提出的问题,以及实际提出的答案。 rant:动词 1. 以愤怒、热情的方式大喊大叫。 最初的答案不是愤怒或热情:这是经过深思熟虑的回应,带有示例。 @JörgWMittag 最初的问题还包括: 上面的构造并没有产生预期的结果,这对我来说似乎很奇怪。这是什么原因?这种行为在什么情况下是合理的?所以他追求的是理由,而不是代码解决方案。 再一次,我看不出grep 的行为在任何方面、形状或形式与迭代空范围是无操作的事实有关。我也看不出迭代一个空范围是一个无操作的事实是如何以任何方式形成或形成“无尽的令人惊讶”和“彻底破碎”的。 因为范围 4..0 具有 [4, 3, 2, 1, 0] 的明显意图,但令人惊讶的是甚至没有发出警告。它让 OP 感到惊讶,也让我感到惊讶,无疑也让许多其他人感到惊讶。我列出了其他令人惊讶的行为示例。如果你愿意,我可以引用更多。一旦某物表现出超过一定数量的令人惊讶的行为,它就会开始飘入“破碎”的领域。有点像常量在被覆盖时如何引发警告,尤其是当方法没有时。【参考方案11】:

这适用于我的懒惰用例

(-999999..0).lazy.map|x| -x.first(3)
#=> [999999, 999998, 999997]

【讨论】:

【参考方案12】:

对我来说最简单的方法是:

[*0..9].reverse

另一种迭代枚举的方式:

(1..3).reverse_each|v| p v

【讨论】:

以上是关于有没有理由我们不能在 ruby​​ 中迭代“反向范围”?的主要内容,如果未能解决你的问题,请参考以下文章

Ruby迭代器

是否有理由我们能够在类定义中定义 [static const int] 而不能在其他静态 const 类型中定义? [复制]

如何使用 Ruby 中的特定步骤迭代反向范围?

没有循环迭代器的枚举

什么是在Ruby中迭代大数字的更快的方法?

相当于 Ruby 中的“继续”