简体   繁体   English

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

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

I understand the basic concept of call-by-name and call-by-value, and I have also looked into handful number of examples.我了解按名称调用和按值调用的基本概念,并且我还研究了一些示例。 However, I am not very clear about when to use call-by-name.但是,我不太清楚何时使用按名称调用。 What would be a real-world scenario where call-by-name would have a significant advantage or performance gain over the other call type?与其他呼叫类型相比,按名称呼叫将具有显着优势或性能提升的真实场景是什么? What should be the correct thinking approach to select a call type while designing a method?在设计方法时选择调用类型的正确思路应该是什么?

There are plenty of places were call-by-name may gain performance or even correctness.有很多地方可以通过名称来获得性能甚至是正确性。

Simple performance example: logging.简单的性能示例:日志记录。 Imagine an interface like this:想象一个这样的界面:

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

And then used like this:然后像这样使用:

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

If the info method doesn't do anything (because, say, the logging level was configured for higher than that), then computeTimeSpent never gets called, saving time.如果info方法不执行任何操作(例如,日志级别配置为高于此级别),则永远不会computeTimeSpent ,从而节省时间。 This happens a lot with loggers, where one often sees string manipulation which can be expensive relative to the tasks being logged.这种情况在记录器中经常发生,人们经常看到字符串操作相对于正在记录的任务而言可能是昂贵的。

Correctness example: logic operators.正确性示例:逻辑运算符。

You have probably seen code like this:你可能见过这样的代码:

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

Say you declared && method like this:假设你声明&&方法是这样的:

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

then, whenever ref is null , you'll get an error because isSomething will be called on a null reference before being passed to && .然后,每当refnull时,您都会收到错误消息,因为isSomething将在传递给&&之前在null引用上调用。 For this reason, the actual declaration is:因此,实际的声明是:

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

So one may actually wonder is when to use call-by-value.所以人们实际上可能想知道何时使用按值调用。 In fact, in the Haskell programming language everything works similar to how call-by-name works (similar, but not the same).事实上,在 Haskell 编程语言中,一切的工作方式都类似于按名称调用的工作方式(类似,但不一样)。

There are good reasons not to use call-by-name: it is slower, it creates more classes (meaning the program takes longer to load), it consumes more memory, and it is different enough that many have difficult reasoning about it.不使用 call-by-name 有很好的理由:它更慢,它创建更多的类(意味着程序需要更长的加载时间),它消耗更多的内存,而且它的不同之处足以让许多人难以理解它。

Call by name means the value is evaluated at the time it is accessed, while with call by value the value is evaluated first and then passed to the method.按名称调用意味着在访问值时对其进行评估,而按值调用则首先对值进行评估,然后将其传递给方法。

To see the difference, consider this example (completely non-functional programming with only side effects ;) ).要查看差异,请考虑这个示例(完全非函数式编程,只有副作用;))。 Say you want to create a function which measures how much time some operation takes.假设您想创建一个函数来测量某些操作需要多少时间。 You can do it with 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)
}

You will get the result (YMMV):你会得到结果(YMMV):

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

But if you change only the signature of measure to measure(action: Unit) so it uses pass-by-value, the result will be:但是,如果您仅将 measure 的签名更改为measure measure(action: Unit)以使其使用按值传递,则结果将是:

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

As you can see, the action is evaluated before measure even starts and also the elapsed time is close to 0 due to the action already having been run before the method was called.如您所见,甚至在measure开始之前就评估了该action ,并且由于在调用该方法之前该操作已经运行,因此经过的时间也接近于 0。

Here, pass-by-name allows the expected behavior of the method to be achieved.在这里,按名称传递允许实现方法的预期行为。 In some cases it doesn't influence the correctness, but does influence the performance, for example in logging frameworks where a complex expression might not need to be evaluated at all if the result is not used.在某些情况下,它不会影响正确性,但会影响性能,例如在日志框架中,如果不使用结果,可能根本不需要评估复杂的表达式。

The simple way it might be explained is可能解释的简单方法是

call-by-value functions compute the passed-in expression's value before calling the function, thus the same value is accessed every time.值调用函数在调用函数之前计算传入表达式的值,因此每次都访问相同的值。 However, call-by-name functions recompute the passed-in expression's value every time it is accessed.但是,按名称调用函数每次访问时都会重新计算传入表达式的值。

I've always thought this terminology is needlessly confusing.我一直认为这个术语是不必要的混乱。 A function can have multiple parameters which vary in their call-by-name vs call-by-value status.一个函数可以有多个参数,它们的名称调用和值调用状态各不相同。 So it's not that a function is call-by-name or call-by-value, it's that each of its parameters may be pass-by-name or pass-by-value.所以不是一个函数是按名称调用或按值调用,而是它的每个参数都可能是按名称传递或按值传递。 Furthermore, "call-by-name" has nothing to do with names.此外,“按名称呼叫”与名称无关。 => Int is a different type from Int; => Int 是与 Int 不同的类型; it's "function of no arguments that will generate an Int" vs just Int.它是“将生成 Int 的无参数函数”与仅 Int 相比。 Once you've got first-class functions you don't need to invent call-by-name terminology to describe this.一旦您拥有一流的功能,您就不需要发明名称调用术语来描述这一点。

When a call-by-name parameter is used more than once in a function, the parameter is evaluated more than once.当在函数中多次使用按名称调用参数时,该参数会被多次计算。

As the parameter being passed in should be a pure function call per Functional Programming, each evaluation within the called function will always generate the same result.由于传入的参数应该是每个函数式编程的纯函数调用,因此被调用函数中的每次评估将始终生成相同的结果。 Therefore, call-by-name would be more wasteful than the conventional call-by-value.因此,按名称调用会比传统的按值调用更浪费。

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

Call by name gets evaluated each time it is called.每次调用时都会评估按名称调用。 But is never evaluated if it is never called.但是如果它从未被调用,则永远不会被评估。 So if your use case is ok with, or needs, new evaluation each time (eg getting fresh/latest data from somewhere) it is useful, and it is never evaluated if never used.因此,如果您的用例每次都可以接受或需要新的评估(例如从某个地方获取新鲜/最新数据),那么它是有用的,并且如果从未使用过,则永远不会被评估。 See code below.请参阅下面的代码。 Remove the "=>" and see what happens.删除“=>”,看看会发生什么。 There will be only one call before the method "m" is called.在调用方法“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