繁体   English   中英

何时使用按名称调用和按值调用?

[英]When to use call-by-name and call-by-value?

我了解按名称调用和按值调用的基本概念,并且我还研究了一些示例。 但是,我不太清楚何时使用按名称调用。 与其他呼叫类型相比,按名称呼叫将具有显着优势或性能提升的真实场景是什么? 在设计方法时选择调用类型的正确思路应该是什么?

有很多地方可以通过名称来获得性能甚至是正确性。

简单的性能示例:日志记录。 想象一个这样的界面:

trait Logger {
  def info(msg: => String)
  def warn(msg: => String)
  def error(msg: => String)
}

然后像这样使用:

logger.info("Time spent on X: " + computeTimeSpent)

如果info方法不执行任何操作(例如,日志级别配置为高于此级别),则永远不会computeTimeSpent ,从而节省时间。 这种情况在记录器中经常发生,人们经常看到字符串操作相对于正在记录的任务而言可能是昂贵的。

正确性示例:逻辑运算符。

你可能见过这样的代码:

if (ref != null && ref.isSomething)

假设你声明&&方法是这样的:

trait Boolean {
  def &&(other: Boolean): Boolean
}

然后,每当refnull时,您都会收到错误消息,因为isSomething将在传递给&&之前在null引用上调用。 因此,实际的声明是:

trait Boolean {
  def &&(other: => Boolean): Boolean =
    if (this) other else this
}

所以人们实际上可能想知道何时使用按值调用。 事实上,在 Haskell 编程语言中,一切的工作方式都类似于按名称调用的工作方式(类似,但不一样)。

不使用 call-by-name 有很好的理由:它更慢,它创建更多的类(意味着程序需要更长的加载时间),它消耗更多的内存,而且它的不同之处足以让许多人难以理解它。

按名称调用意味着在访问值时对其进行评估,而按值调用则首先对值进行评估,然后将其传递给方法。

要查看差异,请考虑这个示例(完全非函数式编程,只有副作用;))。 假设您想创建一个函数来测量某些操作需要多少时间。 您可以通过名称调用来做到这一点:

def measure(action: => Unit) = {
    println("Starting to measure time")
    val startTime = System.nanoTime
    action
    val endTime = System.nanoTime
    println("Operation took "+(endTime-startTime)+" ns")
}

measure {
    println("Will now sleep a little")
    Thread.sleep(1000)
}

你会得到结果(YMMV):

Starting to measure time
Will now sleep a little
Operation took 1000167919 ns

但是,如果您仅将 measure 的签名更改为measure measure(action: Unit)以使其使用按值传递,则结果将是:

Will now sleep a little
Starting to measure time
Operation took 1760 ns

如您所见,甚至在measure开始之前就评估了该action ,并且由于在调用该方法之前该操作已经运行,因此经过的时间也接近于 0。

在这里,按名称传递允许实现方法的预期行为。 在某些情况下,它不会影响正确性,但会影响性能,例如在日志框架中,如果不使用结果,可能根本不需要评估复杂的表达式。

可能解释的简单方法是

值调用函数在调用函数之前计算传入表达式的值,因此每次都访问相同的值。 但是,按名称调用函数每次访问时都会重新计算传入表达式的值。

我一直认为这个术语是不必要的混乱。 一个函数可以有多个参数,它们的名称调用和值调用状态各不相同。 所以不是一个函数是按名称调用或按值调用,而是它的每个参数都可能是按名称传递或按值传递。 此外,“按名称呼叫”与名称无关。 => Int 是与 Int 不同的类型; 它是“将生成 Int 的无参数函数”与仅 Int 相比。 一旦您拥有一流的功能,您就不需要发明名称调用术语来描述这一点。

当在函数中多次使用按名称调用参数时,该参数会被多次计算。

由于传入的参数应该是每个函数式编程的纯函数调用,因此被调用函数中的每次评估将始终生成相同的结果。 因此,按名称调用会比传统的按值调用更浪费。

对于按名称调用的日志记录用例来说很好: https ://www.tutorialspoint.com/scala/functions_call_by_name.htm#:~:text=For%20this%20circumstance%2C%20Scala%20offers,and%20the% 20value%20is%20calculated

每次调用时都会评估按名称调用。 但是如果它从未被调用,则永远不会被评估。 因此,如果您的用例每次都可以接受或需要新的评估(例如从某个地方获取新鲜/最新数据),那么它是有用的,并且如果从未使用过,则永远不会被评估。 请参阅下面的代码。 删除“=>”,看看会发生什么。 在调用方法“m”之前只有一次调用。

object RunMe extends App {

  def m(b: Boolean, m: => String) = {
    if(b) m
  }

  def f = {
    println("hello world")
    "completed"
  }
  println("starting...")
  m(false, f)

}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM