Scala学习笔记——传名参数和传值参数

Scala传名参数和传值参数

文章目录

  • `Scala`传名参数和传值参数
    • `1.` 定义
    • `2.` 两者的比较
    • `3.` 自定义`while`循环


1. 定义

Scala的解释器在解析函数参数(function arguments)时有两种方式:

  • 先计算参数表达式的值(reduce the arguments),再应用到函数内部;或者是将未计算的参数表达式直接应用到函数内部。
  • 前者叫做传值调用(call-by-value),后者叫做传名调用(call-by-name)

例如:

object Add {  
    def addByName(a: Int, b: => Int) = a + b   
    def addByValue(a: Int, b: Int) = a + b   
}

addByName是传名调用,addByValue是传值调用。语法上可以看出,使用传名调用时,在参数名称和参数类型中间有一个" =>"符号。
a2b2+2为例,他们在Scala解释器进行参数规约(reduction)时的顺序分别是这样的:

    addByName(2, 2 + 2)  
->2 + (2 + 2)  
->2 + 4  
->6  
    
    addByValue(2, 2 + 2)  
->addByValue(2, 4)  
->2 + 4  
->6

可以看出,在进入函数内部前,传值调用方式就已经将参数表达式的值计算完毕,而传名调用是在函数内部进行参数表达式的值计算的。
这就造成了一种现象,每次使用传名调用时,解释器都会计算一次表达式的值。对于有副作用(side-effect)的参数来说,这无疑造成了两种调用方式结果的不同。

2. 两者的比较

  • 传值调用在进入函数体之前就对参数表达式进行了计算,这避免了函数内部多次使用参数时重复计算其值,在一定程度上提高了效率。
  • 传名调用的一个优势在于,如果参数在函数体内部没有被使用到,那么它就不用计算参数表达式的值了。在这种情况下,传名调用的效率会高一点。

以一个具体的例子来说明传名参数的用法:

    var assertionsEnabled = true
    def myAssert(predicate: => Boolean ) = {
        if(assertionsEnabled && !predicate)
        throw new AssertionError
    }

这个myAssert函数的参数为一个函数类型,如果标志assertionsEnabledTrue时,mymyAssert 根据predicate 的真假决定是否抛出异常,如果assertionsEnabledfalse,则这个函数什么也不做。

现在可以直接使用下面的语法来调用myNameAssert

    myNameAssert(5 > 3)

看到这里,你可能会想我为什么不直接把参数类型定义为Boolean,比如:

    def boolAssert(predicate: Boolean ) = {
        if(assertionsEnabled && !predicate)
        throw new AssertionError
    }

它的调用也可以使用boolAssert(5 > 3),和myNameAssert 调用看起来也没什么区别,其实两者有着本质的区别,一个是传值参数,一个是传名参数,在调用boolAssert(5 > 3)时,5 > 3是已经计算出为true,然后传递给boolAssert方法,而myNameAssert(5 > 3),表达式5 > 3没有事先计算好传递给myNameAssert,而是先创建一个函数类型的参数值,这个函数的apply方法将计算5 > 3,然后这个函数类型的值作为参数传给myNameAssert

因此这两个函数一个明显的区别是,如果设置assertionsEnabledfalse,然后试图计算 x / 0 == 0

    var assertionsEnabled = false
    val x = 5
    boolAssert( x / 0 == 0) // 报错 java.lang.ArithmeticException: / by zero
    myNameAssert ( x / 0 == 0) // 不报错,因为没有调用predicate,则x / 0 == 0 没有执行

    def myAssert(predicate: => Boolean ) = {
        if(assertionsEnabled && !predicate)
        throw new AssertionError
    }

    def boolAssert(predicate: Boolean ) = {
        if(assertionsEnabled && !predicate)
        throw new AssertionError
    }

可以看到boolAssert 抛出 java.lang.ArithmeticException: / by zero 异常,这是因为这是个传值参数,首先计算 x / 0 ,而抛出异常,而 myNameAssert 没有任何显示,这是因为这是个传名参数,传入的是一个函数类型的值,不会先计算x / 0 == 0,而在myNameAssert 函数体内,由于assertionsEnabledfalse,传入的predicate没有必要计算(短路计算),因此什么也不会打印。如果我们把myNameAssert 修改下,把predicate放在前面:

    def myAssert(predicate: => Boolean ) = {
        if(!predicate && assertionsEnabled )
        throw new AssertionError
    }
    var assertionsEnabled = false
    val x = 5
    boolAssert( x / 0 == 0) // 报错 java.lang.ArithmeticException: / by zero
    myNameAssert ( x / 0 == 0) // 报错 java.lang.ArithmeticException: / by zero

则两个都会报错,因为if里面会先判断predicate

3. 自定义while循环

我们使用的while循环结构如下:

    while(/*条件*/){
      // 代码块
    }

我们可以把它看做为一个有两个参数的函数,即while(flag:Boolean)(block: => Unit),具体实现如下:

object note {
  def main(args: Array[String]): Unit = {
    var i = 0;

    mywhile(i < 10) {
      println(i)
      i += 1
    }
    /* 上面那个循环等价于:

    mywhile(i < 10)(f)
    def f() = {
      println(i)
      i += 1
    }
    */

    def mywhile(flag: Boolean)(block: => Unit): Unit = {
      if (flag) { // 如果flag为真则执行代码块(代码块就是一个函数),并递归调用自己,实现循环的功能
        block
        mywhile(flag)(block)
      } // 如果flag为假,则不执行代码块,并停止循环,即停止递归
    }
  }
}

我们希望它打印09,但是事实并不如此,因为传flag参数的时候是用的传值调用,即一开始值就算完了是(0 < 10 : true),,这会导致递归会一直进行下去,解决这个问题很简单,把flag改成传名调用就行了,如下:

object note {
  def main(args: Array[String]): Unit = {
    var i = 0;
    mywhile(i < 10) {
      println(i)
      i += 1
    }

    def mywhile(flag: => Boolean)(block: => Unit): Unit = {
      if (flag) {
        block
        mywhile(flag)(block)
      }
    }
  }
}
 def mywhile(flag: => Boolean)(block: => Unit): Unit = {
      if (flag) {
        block
        mywhile(flag)(block)
      }
    }
  }
}

你可能感兴趣的:(Scala)