Ruby:块内的yield

5
以下是扫描示例:

这里是一个扫描示例:

"abcdeabcabc".scan("a")

所以它返回一个包含3个“a”的数组。 scan的另一个示例:
"abcdeabcabc".scan("a") {|x| puts x}

这段代码每次输出一个 "a",但依然会输出一个数组,而这次实际返回的却是原始字符串。

因此,从文档和上面的行为来看,scan 要么返回一个数组(没有给出 block),要么在一些副作用发生之前返回原始字符串。重点是两种情况都会返回某些东西。

那么,如果我在 block 中放置一个 "yield",会发生什么?会返回什么? 或者不会返回任何东西?返回值的类型会是什么?

"abcdeabcabc".scan("a") {|x| yield x}

以上内容无法正常工作,因为Ruby会抱怨没有给出块。这对我来说有些合理。但是如果它是类方法的一部分,比如自己实现的“each”方法,那么下面的代码可以正常工作:
class Test
  def my_each
    "abcdeabcabc".scan("a") {|x| yield x}
  end
end
# => :my_each

t = Test.new
# => #<Test:0x007ff00a8d79b0>

t.my_each {|x| puts "so this is #{x}"}
# it works. Outpus 3 a's then return the original string.

那么,Test类的my_each方法的返回值是什么?是yield的列表还是其他什么东西?但是如前所述,“abcdeabcabc”。scan(“a”){|x| yield x}段将被Ruby抱怨,直到给定了一个块。在my_each实现的内部,发生了什么使得my_each的块传递给my_each内部的段落?
3个回答

4
块的传递方式与该函数的参数类似。这可以明确指定,如下所示:
class Test
  def my_each(&block)
    "abcdeabcabc".scan("a") do |x|
      puts "!!! block"
      yield x
      # Could be replaced with: block.call(x)
    end
  end
end

从技术上讲,它完全相同(puts是为了澄清而放置的),它的存在方式不同于通常对参数执行的检查。如果您忘记给它提供代码块,该函数将在第一个必须使用完全相同的LocalJumpError来执行的yield上停止(至少在Rubinius上是这样)。但是,在发生这种情况之前,请注意控制台中的 "!!! block"。

它之所以这样工作,是有原因的。您可以检查函数是否被赋予了代码块,如果像上面明确指定,则使用if block,然后跳过yield。Rails的content_tag助手就是一个很好的例子。此助手的调用可以嵌套块。下面是一个简单的示例:

content_tag :div do
  content_tag :div
end

...生成如下输出:

<div>
  <div></div>
</div>

因此,这个块被执行在你的方法的“顶部”(按照调用堆栈的术语)。每次发生 yield 时,都会将其作为某种函数调用在块上调用。它不会在任何地方累积以便以后执行块。
更新:
由许多迭代器明确构建的 Enumerator 保存了上下文的状态。在许多 each 中返回。
可以像这样在 my_each 上实现:
class Test
  def my_each(&block)
    if block
      "abcdeabcabc".scan("a") { |x| yield x }
    else
      Enumerator.new(self, :my_each)
    end
  end
end

这个答案对我来说更有意义。在运行时,puts实际上会输出一些东西,所以知道它被执行了是很好的,但是和其他答案一样,如果没有给出块的返回值类型仍然让我感到好奇。你介意也分享一些关于这方面的信息吗?谢谢! - Bruce
@Bruce在第一个块执行之前不会发生任何返回。但是,正如你所指出的,许多没有块的each将返回一个Enumerator。这是显式完成的:如果没有给出块,则构造一个Enumerator并附带上下文以重现该each:一个对象,一个迭代器方法及其参数。在irb中尝试:e = 1.upto(5)。检查e - D-side
这可能是静态类型语言和动态类型语言之间的差异之一。如果我理解正确,在第一个块执行之前不会发生任何类型推断。如果没有给出块并且给出了Enumerator的构造,则返回Enumerator类型的值;否则,如果给出了块,则运行进入块并为my_each推断最后一个表达式的类型。那么my_each的最后一个表达式是什么? - Bruce
@Brude 我编辑了答案以反映“枚举器”的性质。 - D-side
@Bruce,再说一遍,将块视为一个参数,一种匿名函数的形式。带有块的方法也会返回一些东西。在scan的情况下,它是处理过的原始字符串。如果存在块,在我的例子中,这将是最后一个表达式。否则,它将是一个枚举器。是的,返回类型并不清楚,但由于差异只在不同的使用场景中才可见,这是最好的方式。 - D-side
显示剩余4条评论

1

"abcdeabcabc".scan("a") {|x| yield x}

在上述代码中,#scan 方法将每个匹配的字符都传递给与之关联的块

现在,在 #scan 的块内部,您正在调用 yield,实际上是调用您传递给方法 my_each 的块。值 x 会传递到您使用方法 my_each 调用时传递的块中。

这太简单了,没有混淆。

Test 类的 my_each 方法的返回值是什么?

根据您当前的代码,返回值应该是 #scan 方法的返回值,这反过来会导致与方法 #my_each 关联的块的最后一个语句的结果(如果被调用),或者是您调用 #scan 方法的接收方

那是一系列的 yield 吗?

是的,yield会被调用,因为#scan方法将找到许多匹配项。

考虑以下示例:

"abcdeabcabc".scan("w") {|x| yield x}

这里与方法#scan相关的不会被调用,因为#scan没有找到任何匹配项,这就是为什么也不会调用yield,结果方法#my_each不会输出传递给方法的块表达式结果,而是"abcdeabcabc"

感谢提供过程描述。也许是因为我刚从类似Java的语言背景下转换过来,所以我卡在了返回值上,因为Ruby声称每个方法调用都会返回某些东西。让我困惑的部分是my_each的调用返回什么。就像"each"一样,如果没有给定块,[1, 2, 4, 5].each会返回一个枚举器。那么"my_each"呢?如果没有块,它会返回什么?还是因为某种原因而简单无效? - Bruce
很棒,我现在看到你的更新了。你最后的例子很有趣,谢谢。在中间你说“是的,yield会被调用...”,但那仍然是一个yield列表吗?那个“yield列表”的类型是什么?由于Ruby是动态类型的,我很想知道Ruby将推断“abcdeabcabc”.scan("a") {|x| yield x}返回的类型是什么。 - Bruce

1

由于给定了一个块来执行scan,因此将返回原始字符串。不管在块内做什么都没有关系。


我知道t.my_each {block}最终会返回原始字符串,但我想问一下my_each本身怎么样?我的意思是,在给它传递块之前。 - Bruce
my_each 与其参数和潜在的块一起评估并依赖于它们。在给定块之前,不存在对 my_each 的评估 - sawa
这正是令人困惑的地方。这是否意味着 Ruby 允许在类方法实现中使用“部分有效”的代码?由于 my_each 本身未指定任何参数,也没有强制使用隐式的最后一个参数(&block),因此该类方法的用户必须知道实现细节(存在“yield”)才能正确使用该方法?出于封装原因,这听起来并不像是一个好的设计。 - Bruce

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接