仍然记得《代码大全2》中作者说过如果所用编程语言中没有提供断言机制,那么就尝试着自己去设计它、实现它,这称为“深入一门语言去编程,不浮于表面”,书中也曾有好几次谈到这个话题。原话是“不要将编程语言局限到所用语言能自动支持的范围。杰出的程序员会考虑他们要干什么,然后才是怎样用手头的的工具去实现它们的目标。”

 

可见,拥有自己看待问题的想法与解决问题的思路是多么重要。而在这里显然作者也是把编程语言看成程序员解决问题的工具。但是下面我们并不讨论这些,我们看看 Scala 中的传名参数如何能够起到类似于断言(assert)的功能(其实 Scala 中已经提供了断言机制),以及看看使用传名参数创建新的控制结构的强大。

 

我们可以定义一个函数 myAssert ,而其参数则为传名参数。在这里,我们想要实现的是断言,可以将传名参数写成函数文本的格式:(…) => Type ,即参数列表 => 类型。例如下面的代码:

 

def myAssert(check: () => Boolean) =

      if(!check()){

             println("OK ...")

             throw new AssertionError

      }

 

上面的函数定义了一个当客户代码传入的函数值(这里我们用()指明,代表省略了该函数的参数列表。函数值被映射(=>)为一个 Boolean 值,然后进行 if 判断。若函数值为假,则断言不通过而抛出 AssertionError 实例。

此时我们可以在 Scala 解释器中输入myAssert(() => 5 < 3) 执行测试一下是否能够使用这个函数。我们会看到如下的输出:

 

scala> myAssert(() => 5 < 3)

OK ...

java.lang.AssertionError

        at .myAssert(<console>:8)

        at .<init>(<console>:7)

 

嗯,虽然有点效果了,但是我们注意到客户端代码中的 () => 5 < 3 似乎有点繁琐,如果能够直接传入 5 < 3 之类的布尔表达式就更好了。这是可以实现的,而且会使得代码更加简洁一些。

 

只需要将函数定义 def 中空参数列表即小括号对()去掉,直接用 => 而不是()=> 就可以了。此外,if 判断中的 check 后面的()也要同时去掉,而以及客户端代码中直接传入一个布尔表达式就可以了。修改后的代码如下:

 

def myAssert(check: => Boolean) =

      if(!check){

             println("OK ...")

             throw new AssertionError

      }

 

myAssert(5 < 3)

 

执行一下,你会发现效果是一样的,就是传名参数使得客户端代码简洁也更容易理解了。但是,我们不免发出疑问,在这个例子中我们干嘛要使用什么传名参数呢?直接对函数中传入个 Boolean 变量再用个 if 语句进行判断不就行了吗?例如下面:

 

// 去掉了 => 映射符号

def myAssert(check: Boolean) =

      if(!check){

             println("OK ...")

             throw new AssertionError

      }

 

是的,我们平时就是这样子的思路。但是,像 Scala 这种具有函数式编程风格的语言,往往我们希望一个函数被调用之后返回一个结果,而不希望函数具有副作用。我们重新写一下上面的常规性函数,以及在下面的代码中,我们都引入一个 Boolean 变量 isValid 用于控制是否开启断言机制。如下:

 

// 实际上我们用 false关闭了断言

val isValid = false

// 传名参数的函数

def byName(check: => Boolean) =

      if(isValid && !check){

             println("byName ...")

             throw new AssertionError

      }

 

// 常规的布尔变量函数

def byBoolean(check: Boolean) =

      if(isValid && !check){

             println("byBoolean ...")

             throw new AssertionError

      }

 

此外,我们分别使用上面两种不同的实现来测试一下原本被我们关闭(因为我们给 isvalid 初始化为 false)的断言函数,如下:

 

scala> byBoolean( 1/0 == 0)

java.lang.ArithmeticException: / by zero

        at .<init>(<console>:8)

        at .<clinit>(<console>)

 

scala> byName( 1/0 == 0)

// 执行后没有任何输出

 

从上面的执行结果可以看出,即使我们关闭了 isValid ,使用常规性的布尔表达式来实现时会在调用 byBoolean 函数之前就先计算了 1 / 0 ,而这一除零操作显然是会抛出ArithmeticException 异常的。这说明了我们对于 isValid 的关闭也是不奏效的。

 

然而,当我们使用 byname 这一传名参数的实现时,发现没有任何输出,说明 isValid 的关闭对于断言机制起到效果了。

 

出现了这种不同的结果,我们当然要对比一下两者在运行时的不同。同时,我们应该知道 && 运算符是具有短路特性的,这是必须肯定的。所以我们才期望当我们关闭了 isValid 之后则表示我们不希望断言起作用,因为对于传入的参数的判断我们是放在了&&后面。

 

其实, byBoolean 这个例子中是由于客户代码中的 1 / 0 出现了其副作用,即它先于 byBoolean 函数的调用而运行。在byName这个例子中,客户代码中的1/0 == 0 则是因为使用了传名参数的机制,使得 byName 函数中 if 判断中的 isValid check 的执行可能存在 isValid 先于 check 的执行。

 

而至于 Scala 对此以一种怎样的先后顺序执行,恐怕只有深入之后才能做出明确的解答吧。也许这就是没有副作用的函数在面对某些问题时所体现出来的一个优点吧。