[英]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
}
然后,每当ref
为null
时,您都会收到错误消息,因为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.