在Scala中按名称调用与按值调用,需要澄清


239

据我了解,在Scala中,一个函数可以称为

  • 按价值或
  • 按名字

例如,给定以下声明,我们是否知道该函数的调用方式?

宣言:

def  f (x:Int, y:Int) = x;

呼叫

f (1,2)
f (23+55,5)
f (12+3, 44*11)

请遵守什么规则?

Answers:


537

您提供的示例仅使用按值调用,因此我将给出一个新的,更简单的示例来显示差异。

首先,假设我们有一个带有副作用的函数。此函数打印出一些内容,然后返回Int

def something() = {
  println("calling something")
  1 // return value
}

现在,我们将定义两个函数,它们接受Int完全相同的参数,只是一个函数采用按值调用样式(x: Int)接受参数,而另一个采用按名字调用样式(x: => Int)的参数。

def callByValue(x: Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

def callByName(x: => Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

现在,当我们使用副作用函数调用它们时会发生什么?

scala> callByValue(something())
calling something
x1=1
x2=1

scala> callByName(something())
calling something
x1=1
calling something
x2=1

因此,您可以看到在按值调用版本中,传入函数调用(something())的副作用仅发生一次。但是,在按姓名呼叫的版本中,副作用发生了两次。

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


296
我一直认为这种术语会造成不必要的混乱。一个函数可以具有多个参数,这些参数的名称调用状态与值调用状态不同。因此,它不是一个功能是通话按姓名或呼叫的价值,那就是它的每一个的参数可以通过逐名或传递的价值。此外,“呼叫按姓名”已无关的名字=> Int是不同的类型Int; 它是“无参数的函数会生成Int” vs just Int。获得一流的功能后,您无需发明按名字呼叫的术语即可对此进行描述。

2
@Ben,可以帮助回答几个问题,谢谢。我希望更多的文章对此进行清楚的解释。
Christopher Poile 2013年

3
@SelimOber如果将文本f(2)编译为type的表达式Int,则生成的代码将f使用arguments 调用,2结果是表达式的值。如果将相同的文本编译为类型的表达式,=> Int则生成的代码将使用对某种“代码块”的引用作为表达式的值。无论哪种方式,都可以将该类型的值传递给需要该类型参数的函数。我很确定您可以通过变量赋值来做到这一点,而不会传递任何参数。那么名称或呼叫与它有什么关系呢?

4
@Ben那么,如果=> Int“没有参数的函数会生成一个Int”,那又有什么不同() => Int呢?Scala似乎将这些区别对待,例如,=> Int显然不能作为a的类型val,而只能作为参数的类型。
蒂姆·古德曼

5
@TimGoodman是的,它比我说的要复杂一些。=> Int这是一种便利,它并不能完全像函数对象那样实现(大概是为什么您不能拥有类型的变量=> Int,尽管没有根本原因不能实现此目的)。() => Int明确的没有参数,将返回的功能Int,这需要显式调用,并且可以作为一个函数被传递。=> Int是某种“代理Int”,您唯一可以做的就是调用它(隐式)以获取Int
2013年

51

这是Martin Odersky的示例:

def test (x:Int, y: Int)= x*x

我们想研究一下评估策略,并确定在以下情况下哪种策略更快(步骤更少):

test (2,3)

按值调用:test(2,3)-> 2 * 2-> 4
按名称调用:test(2,3)-> 2 * 2-> 4
此处以相同的步数达到结果。

test (3+4,8)

按值调用:测试(7,8)-> 7 * 7-> 49
按名称调用:(3 + 4)(3 + 4)-> 7(3 + 4)-> 7 * 7-> 49
此处调用按价值计算更快。

test (7,2*4)

按值调用:test(7,8)-> 7 * 7-> 49
按名称调用:7 * 7-> 49
这里按名称调用更快

test (3+4, 2*4) 

按值调用:test(7,2 * 4)-> test(7,8)-> 7 * 7-> 49
按名称调用:(3 + 4)(3 + 4)-> 7(3 + 4) -> 7 * 7-> 49
在相同步骤中达到结果。


1
在CBV的第三个示例中,我认为您的意思是用test(7,8)代替test(7,14)
talonx 2014年

1
示例来自Coursera,它是Scala编程的原理。讲座1.2。按名称进行的呼叫应def test (x:Int, y: => Int) = x * x注意,从未使用过参数y。
jerry博士

1
好的例子!取自Coursera MOOC :)
alxsimo

这是对区别的很好的解释,但没有解决所问的问题,即两者中哪个是Scala调用的
db1234

16

在您的示例中,所有参数将在函数中调用之前进行求值,因为您仅通过value定义它们。如果要通过名称定义参数则应传递代码块:

def f(x: => Int, y:Int) = x

这样,直到在函数中调用参数后,x才会对参数求值。

这里的这篇小文章也很好地解释了这一点


10

为了重复上述注释中@Ben的观点,我认为最好将“按名称呼叫”仅视为语法糖。解析器只是将表达式包装在匿名函数中,以便可以在以后使用它们时调用它们。

实际上,不是定义

def callByName(x: => Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

并运行:

scala> callByName(something())
calling something
x1=1
calling something
x2=1

您还可以编写:

def callAlsoByName(x: () => Int) = {
  println("x1=" + x())
  println("x2=" + x())
}

并按以下方式运行以达到相同的效果:

callAlsoByName(() => {something()})

calling something
x1=1
calling something
x2=1

我认为您的意思是:<!-语言:lang-scala-> def callAlsoByName(x:()=> Int)= {println(“ x1 =” + x())println(“ x2 =” + x( ))}然后:<!-语言:lang-js-> callAlsoByName(()=> something())我认为您在上一次调用中不需要在something()周围使用花括号。注意:我试图只编辑您的答案,但我的编辑被审稿人拒绝,说它应该是评论或单独的答案。
lambdista 2015年

显然,您不能在注释中使用语法突出显示,因此只需忽略“ <!-language:lang-scala->”部分!我本来可以编辑我自己的评论,但只允许您在5分钟之内完成!:)
lambdista 2015年

1
我最近也遇到了这个问题。可以从概念上这样思考,但scala可以区分=> T() => T。将第一种类型作为参数的函数将不接受第二种,scala将足够的信息存储在@ScalaSignature注释中,从而为此抛出编译时错误。两个字节码=> T() => T是相同的,但并是一个Function0。有关更多详细信息,请参见此问题
vsnyc

6

我将尝试通过一个简单的用例进行解释,而不是仅提供一个示例

想象一下,您想构建一个“ nagger应用程序”,自上次您陷入困境以来,它每次都会困扰您。

检查以下实现:

object main  {

    def main(args: Array[String]) {

        def onTime(time: Long) {
            while(time != time) println("Time to Nag!")
            println("no nags for you!")
        }

        def onRealtime(time: => Long) {
            while(time != time) println("Realtime Nagging executed!")
        }

        onTime(System.nanoTime())
        onRealtime(System.nanoTime())
    }
}

在上面的实现中,nagger仅在按名称传递时起作用,原因是,当按值传递时,它将重新使用,因此不会重新评估值,而按名称传递时,将对每个值进行重新评估访问变量的时间


4

通常,函数的参数是按值参数;也就是说,参数的值是在传递给函数之前确定的。但是,如果我们需要编写一个函数,该函数接受在函数中被调用之前不希望对其求值的表达式作为参数,该怎么办?在这种情况下,Scala提供了按名字呼叫的参数。

逐名呼叫机制将代码块传递给被调用者,并且每次被调用者访问参数时,都会执行该代码块并计算值。

object Test {
def main(args: Array[String]) {
    delayed(time());
}

def time() = {
  println("Getting time in nano seconds")
  System.nanoTime
}
def delayed( t: => Long ) = {
  println("In delayed method")
  println("Param: " + t)
  t
}
}
 1. C:/> scalac Test.scala 
 2.scala测试
 3.延迟方式
 4.时间以纳秒为单位
 5.参数:81303808765843
 6.获得时间(以纳秒为单位)

2

正如我所假设的,call-by-value上面讨论的函数只是将值传递给函数。根据Martin Odersky这是一种评估策略,紧随其后的Scala在功能评估中起着重要作用。但是,使其变得简单call-by-name。就像传递函数作为方法的参数一样Higher-Order-Functions。当方法访问传递的参数的值时,它将调用传递的函数的实现。如下:

根据@dhg示例,首先将方法创建为:

def something() = {
 println("calling something")
 1 // return value
}  

此函数包含一个println语句并返回一个整数值。创建函数,其参数为call-by-name

def callByName(x: => Int) = {
 println("x1=" + x)
 println("x2=" + x)
}

此函数参数定义了一个匿名函数,该函数返回一个整数值。其中x包含函数的定义,这些函数的定义已0传递参数但返回int值,并且我们的something函数包含相同的签名。调用函数时,会将函数作为参数传递给callByName。但是在call-by-value仅将整数值传递给函数的情况下。我们将函数调用如下:

scala> callByName(something())
 calling something
 x1=1
 calling something
 x2=1 

在此something方法中,我们的方法被调用了两次,因为当我们访问xin callByName方法的值时,它将调用something方法的定义。


2

按值调用是一般用例,如此处许多答案所述。

按名称呼叫将代码块传递给呼叫者,并且每次呼叫者访问参数时,都会执行该代码块并计算值。

我将在下面的用例中尝试以更简单的方式演示按名称进行呼叫

范例1:

在函数下面是按名称进行调用的简单示例/用例,该函数以函数为参数并给出经过的时间。

 /**
   * Executes some code block and prints to stdout the 
time taken to execute   the block 
for interactive testing and debugging.
   */
  def time[T](f: => T): T = {
    val start = System.nanoTime()
    val ret = f
    val end = System.nanoTime()

    println(s"Time taken: ${(end - start) / 1000 / 1000} ms")

    ret
  }

范例2:

阿帕奇火花(具有阶)使用使用由名称方式呼叫见登录Logging性状 其中其懒惰地求值是否log.isInfoEnabled或不从下面的方法。

protected def logInfo(msg: => String) {
     if (log.isInfoEnabled) log.info(msg)
 }

2

“按值调用 ”中,表达式的值在函数调用时预先计算,并且该特定值作为参数传递给相应的函数。整个功能将使用相同的值。

而在“按名称调用”中,表达式本身作为参数传递给函数,并且仅在调用该特定参数时才在函数内部进行计算。

通过以下示例,可以更好地理解Scala中按名称调用和按值调用之间的区别:

代码段

object CallbyExample extends App {

  // function definition of call by value
  def CallbyValue(x: Long): Unit = {
    println("The current system time via CBV: " + x);
    println("The current system time via CBV " + x);
  }

  // function definition of call by name
  def CallbyName(x: => Long): Unit = {
    println("The current system time via CBN: " + x);
    println("The current system time via CBN: " + x);
  }

  // function call
  CallbyValue(System.nanoTime());
  println("\n")
  CallbyName(System.nanoTime());
}

输出量

The current system time via CBV: 1153969332591521
The current system time via CBV 1153969332591521


The current system time via CBN: 1153969336749571
The current system time via CBN: 1153969336856589

在上面的代码片段中,对于函数调用CallbyValue(System.nanoTime()),系统已预先计算了纳米时间,并且该预先计算的值已将参数传递给函数调用。

但是在CallbyName(System.nanoTime())函数调用中,表达式“ System.nanoTime())本身作为参数传递给函数调用,并且当在函数内部使用该参数时,将计算该表达式的值。

注意CallbyName函数的函数定义,其中有一个=>符号分隔参数x及其数据类型。那里的那个特殊符号表示该函数是按名称类型进行调用的。

换句话说,“按值调用”函数自变量在进入函数之前先评估一次,但“按名称调用”函数自变量仅在需要时才在函数内部求值。

希望这可以帮助!


2

这是我编写的一个简单示例,旨在帮助我的一名当前正在学习Scala课程的同事。我认为很有趣的是,马丁没有使用讲座前面介绍的&&问题作为示例。无论如何,我希望这会有所帮助。

val start = Instant.now().toEpochMilli

val calc = (x: Boolean) => {
    Thread.sleep(3000)
    x
}


def callByValue(x: Boolean, y: Boolean): Boolean = {
    if (!x) x else y
}

def callByName(x: Boolean, y: => Boolean): Boolean = {
    if (!x) x else y
}

new Thread(() => {
    println("========================")
    println("Call by Value " + callByValue(false, calc(true)))
    println("Time " + (Instant.now().toEpochMilli - start) + "ms")
    println("========================")
}).start()


new Thread(() => {
    println("========================")
    println("Call by Name " + callByName(false, calc(true)))
    println("Time " + (Instant.now().toEpochMilli - start) + "ms")
    println("========================")
}).start()


Thread.sleep(5000)

代码的输出如下:

========================
Call by Name false
Time 64ms
========================
Call by Value false
Time 3068ms
========================

1

参数通常按值传递,这意味着它们将在函数体内被替换之前先进行评估。

定义函数时,可以通过使用双箭头强制通过名称调用参数。

// first parameter will be call by value, second call by name, using `=>`
def returnOne(x: Int, y: => Int): Int = 1

// to demonstrate the benefits of call by name, create an infinite recursion
def loop(x: Int): Int = loop(x)

// will return one, since `loop(2)` is passed by name so no evaluated
returnOne(2, loop(2))

// will not terminate, since loop(2) will evaluate. 
returnOne(loop(2), 2) // -> returnOne(loop(2), 2) -> returnOne(loop(2), 2) -> ... 

1

互联网上已经有很多关于这个问题的奇妙答案。我将编写一些有关该主题的解释和示例的汇编,以防万一有人发现它会有所帮助

介绍

按价值致电(CBV)

通常,函数的参数是按值调用参数。也就是说,在评估函数本身之前,从左至右评估参数以确定其值

def first(a: Int, b: Int): Int = a
first(3 + 4, 5 + 6) // will be reduced to first(7, 5 + 6), then first(7, 11), and then 7

姓名呼叫(CBN)

但是,如果我们需要编写一个函数来接受在函数中被调用之前不求值的表达式作为参数,该怎么办?在这种情况下,Scala提供了按名字呼叫的参数。意味着参数按原样传递到函数中,并且其赋值在替换后进行

def first1(a: Int, b: => Int): Int = a
first1(3 + 4, 5 + 6) // will be reduced to (3 + 4) and then to 7

逐名呼叫机制将代码块传递给该呼叫,并且每次该呼叫访问该参数时,都会执行该代码块并计算值。在以下示例中,delayed打印一条消息,表明已输入该方法。接下来,延迟打印一条带有其值的消息。最后,延迟返回“ t”:

 object Demo {
       def main(args: Array[String]) {
            delayed(time());
       }
    def time() = {
          println("Getting time in nano seconds")
          System.nanoTime
       }
       def delayed( t: => Long ) = {
          println("In delayed method")
          println("Param: " + t)
       }
    }

在延迟方法中,
获取时间以纳秒为单位
参数:2027245119786400

每种情况的利弊

CBN: +终止的频率更高*在终止之上检查以下内容* +优点是,如果在函数体的评估中未使用相应的参数,则不评估函数参数-速度较慢,它创建的类更多(这意味着程序需要加载时间更长),并且占用更多内存。

CBV: +它通常比CBN指数效率更高,因为它避免了按名称重复调用参数表达式的这种重复计算。它仅对每个函数参数求值一次+它在命令式效果和副作用中发挥更好的作用,因为您倾向于更好地了解何时对表达式求值。-在参数评估过程中可能会导致循环*检查上述终止条件*

如果不能保证终止怎么办?

-如果表达式e的CBV评估终止,则e的CBN评估也终止-另一个方向不正确

非终止示例

def first(x:Int, y:Int)=x

首先考虑表达式(1,循环)

CBN:first(1,loop)→1 CBV:first(1,loop)→减少该表达式的参数。由于一个是循环,因此会无限地减少参数。它不会终止

每种情况下的行为差异

让我们定义一个方法测试

Def test(x:Int, y:Int) = x * x  //for call-by-value
Def test(x: => Int, y: => Int) = x * x  //for call-by-name

案例1测试(2,3)

test(2,3)2*24

由于我们从已经求值的参数开始,因此按值调用和按名称调用将花费相同的步骤

案例2测试(3 + 4,8)

call-by-value: test(3+4,8) → test(7,8)7 * 749
call-by-name: (3+4)*(3+4)7 * (3+4)7 * 749

在这种情况下,按值致电会执行较少的步骤

Case3测试(7,2 * 4)

call-by-value: test(7, 2*4) → test(7,8)7 * 749
call-by-name: (7)*(7)49

我们避免不必要地计算第二个参数

Case4测试(3 + 4,2 * 4)

call-by-value: test(7, 2*4) → test(7,8)7 * 749
call-by-name: (3+4)*(3+4)7*(3+4)7*749

不同的方法

首先,假设我们有一个带有副作用的函数。此函数打印出一些内容,然后返回一个Int值。

def something() = {
  println("calling something")
  1 // return value
}

现在,我们将定义两个函数,它们接受完全相同的Int参数,只是一个函数采用按值调用样式(x:Int)接受另一个参数,而另一个采用按名称调用样式(x: =>整数)。

def callByValue(x: Int) = {
  println("x1=" + x)
  println("x2=" + x)
}
def callByName(x: => Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

现在,当我们使用副作用函数调用它们时会发生什么?

scala> callByValue(something())
calling something
x1=1
x2=1
scala> callByName(something())
calling something
x1=1
calling something
x2=1

因此,您可以看到在按值调用版本中,传入的函数调用(something())的副作用仅发生一次。但是,在按姓名呼叫的版本中,副作用发生了两次。

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

更好地使用按姓名拨打电话的示例

来自:https : //stackoverflow.com/a/19036068/1773841

简单的性能示例:日志记录。

让我们想象一个这样的接口:

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) this else other
}

1

通过示例可以帮助您更好地理解差异。

让我们定义一个返回当前时间的简单函数:

def getTime = System.currentTimeMillis

现在,我们将按名称定义一个函数,该函数将打印两次,延迟一秒钟:

def getTimeByName(f: => Long) = { println(f); Thread.sleep(1000); println(f)}

和一个

def getTimeByValue(f: Long) = { println(f); Thread.sleep(1000); println(f)}

现在让我们分别调用:

getTimeByName(getTime)
// prints:
// 1514451008323
// 1514451009325

getTimeByValue(getTime)
// prints:
// 1514451024846
// 1514451024846

结果应说明差异。该代码段在此处可用。


0

CallByName在使用时被调用,并且callByValue在遇到该语句时被调用。

例如:-

我有一个无限循环,即如果您执行此功能,我们将永远不会得到scala提示。

scala> def loop(x:Int) :Int = loop(x-1)
loop: (x: Int)Int

一个callByName函数采用上述loop方法作为一个参数,它是从未其体内使用。

scala> def callByName(x:Int,y: => Int)=x
callByName: (x: Int, y: => Int)Int

在执行callByName方法时,我们没有发现任何问题(我们会得到scala提示),因为我们在函数内部没有使用循环函数的地方callByName

scala> callByName(1,loop(10))
res1: Int = 1
scala> 

一个callByValue函数将上述loop方法作为参数,因为函数或表达式内部的结果在loop递归执行的函数执行外部函数之前先被求值,而我们永远也不会得到scala提示。

scala> def callByValue(x:Int,y:Int) = x
callByValue: (x: Int, y: Int)Int

scala> callByValue(1,loop(1))

0

看到这个:

    object NameVsVal extends App {

  def mul(x: Int, y: => Int) : Int = {
    println("mul")
    x * y
  }
  def add(x: Int, y: Int): Int = {
    println("add")
    x + y
  }
  println(mul(3, add(2, 1)))
}

y:=> Int是按名称调用。作为名称传递的呼叫是add(2,1)。这将被懒惰地评估。因此,控制台上的输出将是“ mul”,然后是“ add”,尽管add似乎首先被调用。按名称调用就像传递函数指针一样。
现在从y:=> Int更改为y:Int。控制台将显示“ add”,然后显示“ mul”!常用的评估方式。


-2

我不认为这里的所有答案都具有正确的理由:

在按值调用中,参数仅计算一次:

def f(x : Int, y :Int) = x

// following the substitution model

f(12 + 3, 4 * 11)
f(15, 4194304)
15

您可以在上面看到,通常不需要的所有参数都会被求值 call-by-value可以很快,但并非总是如此。

如果采用评估策略,call-by-name则分解将是:

f(12 + 3, 4 * 11)
12 + 3
15

如您在上面看到的,我们不需要评估4 * 11,因此节省了一些计算,有时这可能是有益的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.