我们有理由不能迭代红宝石的“反向范围”吗?


104

我试图使用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构造中也看到了类似的行为,并且在有用时仍然不干净。
fifigyuri 2010年

1
范围本身是按约定返回的。由于该.each语句没有修改任何内容,因此没有要计算的“结果”返回。在这种情况下,Ruby通常会在成功和nil出错时返回原始对象。这使您可以将这样的表达式用作if语句的条件。
bta 2010年

Answers:


99

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

如果要在这样的范围内向后迭代,可以始终使用downto

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

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

以下是其他人的一些想法这些想法为何既难于允许迭代又很难始终处理反向范围。


10
我认为从1到100或从100到1的范围内进行迭代直观上意味着使用步骤1。如果有人想要其他步骤,请更改默认值。同样,对我来说(至少)从1月1日到8月16日进行迭代意味着要逐步进行。我认为通常可以达成共识,因为我们直觉上是这样说的。感谢您的回答,您提供的链接也很有用。
fifigyuri 2010年

3
我仍然认为要为多个范围定义“直观”迭代很难做到一致,而且我不同意在日期上进行迭代以直观地暗示等于1天的步骤-毕竟,一天本身已经是一个范围时间(从午夜到午夜)。例如,谁会说“ 1月1日至8月18日”(恰好20周)并不意味着是几周而不是几天?为什么不按小时,分钟或秒进行迭代?
约翰·费米内拉

8
.each那里是多余的,5.downto(1) { |n| puts n }工作正常。此外,不要执行所有r.first r.last东西,而要执行(6..10).reverse_each
2010年

@ Mk12:100%同意,我只是为了新的Rubyists而变得非常明确。不过,也许这太令人困惑了。
约翰·费米内拉

尝试在表单中添加年份时,我使用了:= f.select :model_year, (Time.zone.now.year + 1).downto(Time.zone.now.year - 100).to_a
Eric Norcross


18

在Ruby中遍历一个范围,并对该范围内的第一个对象each调用succ方法。

$ 4.succ
=> 5

5不在范围内。

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

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

约翰指出,如果跨度为0,这将无法正常工作。这将:

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

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


2
不可以,但是可以乘以-1而不是使用.abs。
乔纳斯·艾夫斯特伦(JonasElfström)2010年

12

根据“ Ruby编程”一书,Range对象存储范围的两个端点,并使用该.succ成员生成中间值。根据您在范围内使用的数据类型的不同,您始终可以创建的子类Integer并重新定义.succ成员,使其像反向迭代器一样工作(您可能还希望重新定义.next)。

您也可以在不使用范围的情况下获得所需的结果。试试这个:

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

这将以-1为步长从4变为0。但是,我不知道这是否适用于除Integer参数之外的任何内容。



5

您甚至可以使用for循环:

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

打印:

4
3
2
1
0

3

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


2
IMO通常认为它很大是很好的。我认为这是正确的信念和习惯。而且由于魔鬼永不眠,我不相信自己,记得在数组上迭代的地方。但是您是对的,如果我们有0和4常数,则遍历数组可能不会引起任何问题。
fifigyuri 2010年

1

正如bta所说,原因是Range#each发送succ到它的开始,然后发送到该succ调用的结果,依此类推,直到结果大于结束值为止。调用不能使数值从4变为0 succ,实际上,您的起点已经大于终点。


1

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

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

0

这适用于我的懒惰用例

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

0

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
 ^

您可能希望<<和push对于追加到数组是相同的,但是

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不支持“合并”,因此您必须编写

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

作为奖励,如果您改变主意,应该重新定义您或他人的常量。


这不会以任何方式,形状或形式回答问题。对作者不喜欢Ruby的事情只不过是a之以鼻。
约尔格W¯¯米塔格

已更新,除了实际提出的答案以外,还回答了未提出的问题。rant:动词1.以愤怒,热情的方式大声说话或大喊。最初的答案不是生气或同情:这是一个带有示例的深思熟虑的回答。
android.weasel

@JörgWMittag原始问题还包括:上面的构造没有产生预期的结果,这让我感到奇怪。是什么原因呢?这种行为是合理的情况是什么?所以他是有原因的,而不是代码解决方案。
android.weasel

再次,我看不到grep与在空范围内进行迭代是无操作这一事实相关的行为,形状或形式如何。我也看不出在空范围内进行迭代是无操作的事实,无论如何以某种形式或形式“无止境地令人惊讶”和“彻底破灭”。
约尔格W¯¯米塔格

因为范围4..0具有[4,3,2,1,0]的明显意图,但令人惊讶的是甚至没有发出警告。它使OP感到惊讶,也使我感到惊讶,并且无疑使其他许多人感到惊讶。我列举了其他令人惊讶的行为的例子。如果您愿意,我可以列举更多。一旦某种事物表现出超出一定数量的令人惊讶的行为,它就会开始漂移到“破碎”的领域。有点像常量如何在覆盖时发出警告,尤其是在方法没有覆盖时。
android.weasel

0

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

[*0..9].reverse

迭代枚举的另一种方法:

(1..3).reverse_each{|v| p v}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.