0

我不清楚为什么在调用函数时需要传递块参数。为什么不直接作为函数参数传入,块参数会发生什么,它们是如何传递和使用的?

m.call(somevalue) {|_k, v| v['abc'] = 'xyz'}

module m 
  def call ( arg1, *arg2, &arg3)

  end
end
4

1 回答 1

3

与几乎所有主流编程语言一样,Ruby 是一门严格的语言,这意味着在将参数传递给方法之前会对其进行全面评估。

现在,假设您要实现(的简化版本)Integer#times. 实现看起来有点像这样:

class Integer
  def my_times(action_to_be_executed)
    raise ArgumentError, "`self` must be non-negative but is `#{inspect}`" if negative?
    return if zero?

    action_to_be_executed

    pred.my_times(action_to_be_executed)
  end
end

3.my_times(puts "Hello")
# Hello

0.my_times(puts "Hello")
# Hello

-1.my_times(puts "Hello")
# Hello
# ArgumentError (`self` must be non-negative but is `-1`)

如您所见,仅3.my_times(puts "Hello")打印Hello一次,而不是应执行的三次。此外,它应该只0.my_times(puts "Hello")打印Hello一次,而不是根本不打印,尽管它return位于方法的第二行,因此action_to_be_executed甚至从未被评估过。甚至只-1.my_times(puts "Hello")打印Hello一次,尽管事实上它作为方法中的第一件事raise是一个ArgumentError例外,因此从不评估方法体的整个其余部分。

这是为什么?因为 Ruby 很严格!同样,严格意味着参数在被传递之前被完全评估。所以,这意味着在被调用之前my_timesputs "Hello"被评估(打印Hello到标准输出流),并且评估的结果(这只是nil因为Kernel#puts总是返回nil)被传递到方法中。

因此,我们需要做的是以某种方式延迟对参数的评估。我们知道如何延迟评估的一种方法是使用方法:仅在调用方法时才评估方法。

因此,我们从 Java 的剧本中取出一页,并定义一个单一抽象方法协议:传递给的参数my_each必须是实现具有特定名称的方法的对象。让我们称之为它call,因为,好吧,我们将称之为它。

这看起来有点像这样:

class Integer
  def my_times(action_to_be_executed)
    raise ArgumentError, "`self` must be non-negative but is `#{inspect}`" if negative?
    return if zero?

    action_to_be_executed.call

    pred.my_times(action_to_be_executed)
  end
end

def (hello = Object.new).call
  puts "Hello"
end

3.my_times(hello)
# Hello
# Hello
# Hello

0.my_times(hello)

-1.my_times(hello)
# ArgumentError (`self` must be non-negative but is `-1`)

好的!有用!当然,传递的参数在传递之前仍然是严格评估的(我们不能从 Ruby 本身改变 Ruby 的基本性质),但是这种评估只会导致对象被局部变量绑定hello。我们要运行的代码是另一个间接层,并且只会在我们实际调用它的地方执行。

它还有另一个优点:Integer#times实际上使当前迭代的索引作为参数可用于操作。这在我们的第一个解决方案中是不可能实现的,但在这里我们可以做到,因为我们使用的是方法并且方法可以接受参数:

class Integer
  def my_times(action_to_be_executed)
    raise ArgumentError, "`self` must be non-negative but is `#{inspect}`" if negative?

    __my_times_helper(action_to_be_executed)
  end

  protected

  def __my_times_helper(action_to_be_executed, index = 0)
    return if zero?

    action_to_be_executed.call(index)

    pred.__my_times_helper(action_to_be_executed, index + 1)
  end
end

def (hello = Object.new).call(i)
  puts "Hello from iteration #{i}"
end

3.my_times(hello)
# Hello from iteration 0
# Hello from iteration 1
# Hello from iteration 2

0.my_times(hello)

-1.my_times(hello)
# ArgumentError (`self` must be non-negative but is `-1`)

但是,这实际上不是很可读。如果你不想给我们试图传递的这个动作命名,而是简单地将它写在参数列表中,它看起来像这样:

3.my_times(Object.new.tap do |obj|
  def obj.call(i)
    puts "Hello from iteration #{i}"
  end
end)
# Hello from iteration 0
# Hello from iteration 1
# Hello from iteration 2

或在一行上:

3.my_times(Object.new.tap do |obj| def obj.call; puts "Hello from iteration #{i}" end end)
# Hello from iteration 0
# Hello from iteration 1
# Hello from iteration 2

# or:

3.my_times(Object.new.tap {|obj| def obj.call; puts "Hello from iteration #{i}" end })
# Hello from iteration 0
# Hello from iteration 1
# Hello from iteration 2

现在,我不了解你,但我觉得这很丑。

在 Ruby 1.9 中,Ruby 向该语言添加了Proc字面量,也就是stabby lambda 字面量。Lambda 文字是一种简洁的文字语法,用于编写带有call方法的Proc对象,特别是带有Proc#call.

使用 lambda 字面量,并且不对我们现有的代码进行任何更改,它看起来像这样:

3.my_times(-> i { puts "Hello from iteration #{i}" })
# Hello from iteration 0
# Hello from iteration 1
# Hello from iteration 2

这看起来不错!

当 Yukihiro “matz” Matsumoto 大约 30 年前的 1993 年初设计 Ruby 时,他对 Smalltalk、Scheme 和 Common Lisp 等语言的核心库和标准库进行了调查,以弄清楚这些方法如何将一段代码作为实际使用了一个参数,他发现绝大多数此类方法只采用一个代码参数,而他们对这个参数所做的只是调用它。

因此,他决定为包含代码且只能调用的单个参数添加特殊语言支持。这个论点在句法和语义上都是轻量级的,特别是,它在句法上看起来与任何其他控制结构完全一样,而且它在语义上不是一个对象。

这个特殊的语言特性,你可能猜到了,是blocks

Ruby 中的每个方法都有一个可选的块参数。我总是可以将一个块传递给一个方法。对块做任何事情都取决于方法。例如,这里的块是无用的,因为Kernel#puts它没有对块做任何事情:

puts("Hello") { puts "from the block" }
# Hello

因为块不是对象,所以不能对它们调用方法。另外,因为块参数只能有一个,所以没有必要给它一个名字:如果你引用一个块,总是很清楚是哪个块,因为只能有一个。但是,如果块没有方法,也没有名字,我们怎么称呼它呢?

这就是yield关键字的用途。它暂时将控制流“交给”块,或者换句话说,它调用块。

使用blocks,我们的解决方案将如下所示:

class Integer
  def my_times(&action_to_be_executed)
    raise ArgumentError, "`self` must be non-negative but is `#{inspect}`" if negative?
    return enum_for(__callee__) unless block_given?

    __my_times_helper(&action_to_be_executed)
  end

  protected

  def __my_times_helper(&action_to_be_executed, index = 0)
    return if zero?

    yield index

    pred.__my_times_helper(&action_to_be_executed, index + 1)
  end
end

3.my_times do 
  puts "Hello from iteration #{i}"
end
# Hello from iteration 0
# Hello from iteration 1
# Hello from iteration 2

0.my_times do 
  puts "Hello from iteration #{i}"
end

-1.my_times do 
  puts "Hello from iteration #{i}"
end
# ArgumentError (`self` must be non-negative but is `-1`)

好的,您可能会注意到,当我在上面写到您可以对块做的唯一事情就是调用它时,我做了一些简化。你可以用它做另外两件事:

  1. 您可以使用 . 检查是否传递了块参数Kernel#block_given?。由于块始终是可选的,并且块没有名称,因此必须有一种方法来检查块是否通过。

  2. 您可以将一个块(不是对象且没有名称)“卷起”到一个Proc对象(它&一个对象)中,并使用与号一元前缀 sigil将其绑定到一个参数(给它一个名称)在方法的参数列表中。现在我们有了一个对象和一种引用它的方法,我们可以将它存储在一个变量中,从一个方法中返回它,或者(就像我们在这里所做的那样)将它作为参数传递给另一个方法,否则不可能。

    还有一种相反的操作:使用与&号一元前缀操作符,你可以将一个Proc对象“展开”成一个参数列表中的一个块;这使得该方法的行为就像您已将存储在 中的代码Proc作为文字块参数传递给该方法一样。

你有它!这就是块的用途:将代码传递给方法的语义和语法轻量级形式。

当然,还有其他可能的方法。最接近 Ruby 的方法可能是 Smalltalk。Smalltalk 还有一个叫做的概念(事实上,Ruby 的想法和名称就是从这里得到的)。与 Ruby 类似,Smalltalk 块具有语法上轻量级的文字形式,但它们是对象,您可以将多个对象传递给一个方法。由于 Smalltalk 的一般轻量级和简单的语法,尤其是散布在方法名称和参数的部分的关键字方法语法,即使将多个块传递给方法调用也是非常简洁和可读的。

例如,Smalltalk 实际上没有if//条件表达式thenelse实际上,Smalltalk 根本没有控制结构。一切都是用方法完成的。因此,条件的工作方式是两个布尔类TrueClassFalseClass每个都有一个名为的方法,该方法ifTrue:ifFalse:接受两个块参数,并且两个实现将简单地评估第一个或第二个块。例如,其中的实现TrueClass可能看起来有点像这样(请注意,Smalltalk 没有类或方法的语法,而是通过 GUI 创建类对象和方法对象在 IDE 中创建类和方法):

True>>ifTrue: trueBlock ifFalse: falseBlock
   "Answer with the value of `trueBlock`."

   ↑trueBlock value

相应的实现FalseClass如下所示:

FalseClass>>ifTrue: trueBlock ifFalse: falseBlock
   "Answer with the value of `falseBlock`."

   ↑falseBlock value

你会这样称呼它:

2 < 3 ifTrue: [ Transcript show: 'yes' ] ifFalse: [ Transcript show: 'no' ].
"yes"

4 < 3 ifTrue: [ Transcript show: 'yes' ] ifFalse: [ Transcript show: 'no' ].
"no"

在 ECMAScript 中,您可以简单地将函数定义用作表达式,并且函数也有轻量级语法。

在各种 Lisps 中,代码就是数据,数据就是代码,所以你可以将代码作为参数作为数据传递,然后在函数内部,再次将该数据视为代码。

Scala 具有按名称调用的参数,这些参数仅在您使用它们的名称时进行评估,并且每次您使用它们的名称时都会对其进行评估。它看起来像这样:

implicit class IntegerTimes(val i: Int) extends AnyVal {
  @scala.annotation.tailrec
  def times(actionToBeExecuted: => Unit): Unit = {
    if (i < 0) throw new Error()
    if (i == 0) () else { actionToBeExecuted; (i - 1).times(actionToBeExecuted) }
  }
}

3.times { println("Hello") }
// Hello
// Hello
// Hello
于 2020-05-07T07:31:32.440 回答