慎用Scala中的return表達式

慎用Scala中的return表達式

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。

手動碼字不易,請大家尊重勞動成果,謝謝

作者:http://blog.csdn.net/wang_wbq

The return keyword is not “optional” or “inferred”; it changes the meaning of your program, and you should never use it.

由於Scala是一種基於JVM的語言,因此大多數程序員都是從Java轉過來的,因此可能會習慣於使用return來寫代碼。

但是Scala作爲一種支持函數式編程的語言,是不推薦大家使用return表達式的,下面通過幾個例子來看下Scala中return可能會帶來的錯誤。

首先先看一個例子:

object ScalaReturn{
  def main(args: Array[String]): Unit = {
    println("func1:" + func1())
    println("func2:" + func2())
  }

  def func1(): Int = {
    def func_inner(i: Int): Int = {
      if (i == 0) return -1
      return i+1
    }
    func_map(func_inner)
  }


  def func2(): Int ={
    val func_Inner: Int => Int = i => {
      if (i == 0) return -1
      return i+1
    }
    func_map(func_Inner)
  }

  def func_map(f: Int => Int): Int = {
    0 to 10 map f sum
  }
}

這裏我寫了兩個一模一樣的的函數func1func2,其中只有func_inner函數的定義一點細微的差別,在func_inner裏我使用了大家常用的return表達式來返回函數結果。func_map函數我使用了很好(nan)看(dong)的寫法,其實就是把0到10這11個數字分別應用f函數,最後將函數返回值求和。

大家認爲運行main函數會打印什麼結果?

是否是大家期待的func1:64 func2:64這個結果?

然而並不是,我運行出來的結果是這樣的:

這裏寫圖片描述

從運行結果來看,func1的運行結果是符合預期的,func2的運行結果就有些奇怪了。

爲了瞭解問題出現的原因,我們還是求助我們的老朋友javap

我們先來看下func1func_Inner函數的編譯後的結果:

  public int func1();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: new           #56                 // class ScalaReturn$$anonfun$func1$1
         4: dup
         5: invokespecial #57                 // Method ScalaReturn$$anonfun$func1$1."<init>":()V
         8: invokevirtual #61                 // Method func_map:(Lscala/Function1;)I
        11: ireturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   LScalaReturn$;
      LineNumberTable:
        line 12: 0
  public final int ScalaReturn$$func_inner$1(int);
    descriptor: (I)I
    flags: ACC_PUBLIC, ACC_FINAL
    Code:
      stack=2, locals=2, args_size=2
         0: iload_1
         1: iconst_0
         2: if_icmpne     7
         5: iconst_m1
         6: ireturn
         7: iload_1
         8: iconst_1
         9: iadd
        10: ireturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   LScalaReturn$;
            0      11     1     i   I
      LineNumberTable:
        line 9: 0
        line 10: 7
      StackMapTable: number_of_entries = 1
        frame_type = 7 /* same */

完全中規中矩,和我們預期的完全一樣。Scala對於函數內的函數就直接把它重命名到了外部(如果引用了外部變量,則會把變量增加在函數參數裏),這個和Java的lambda表達式有點類似。

我們再來看下func2func_Inner函數的編譯後的結果:

  public final scala.runtime.Nothing$ apply(int);
    descriptor: (I)Lscala/runtime/Nothing$;
    flags: ACC_PUBLIC, ACC_FINAL
    Code:
      stack=5, locals=2, args_size=2
         0: iload_1
         1: iconst_0
         2: if_icmpne     18
         5: new           #23                 // class scala/runtime/NonLocalReturnControl$mcI$sp
         8: dup
         9: aload_0
        10: getfield      #25                 // Field nonLocalReturnKey1$1:Ljava/lang/Object;
        13: iconst_m1
        14: invokespecial #29                 // Method scala/runtime/NonLocalReturnControl$mcI$sp."<init>":(Ljava/lang/Object;I)V
        17: athrow
        18: new           #23                 // class scala/runtime/NonLocalReturnControl$mcI$sp
        21: dup
        22: aload_0
        23: getfield      #25                 // Field nonLocalReturnKey1$1:Ljava/lang/Object;
        26: iload_1
        27: iconst_1
        28: iadd
        29: invokespecial #29                 // Method scala/runtime/NonLocalReturnControl$mcI$sp."<init>":(Ljava/lang/Object;I)V
        32: athrow
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  this   LScalaReturn$$anonfun$1;
            0      33     1     i   I
      LineNumberTable:
        line 18: 0
        line 19: 18
      StackMapTable: number_of_entries = 1
        frame_type = 18 /* same */

從上面的代碼中我們可以看出,它的返回是使用throw NonLocalReturnControl$mcI$sp(nonLocalReturnKey1$1, returnValue)來實現的,有拋出異常必定有catch異常,那就是在func2函數裏:

  public int func2();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=1
         0: new           #4                  // class java/lang/Object
         3: dup
         4: invokespecial #64                 // Method java/lang/Object."<init>":()V
         7: astore_1
         8: new           #66                 // class ScalaReturn$$anonfun$1
        11: dup
        12: aload_1
        13: invokespecial #68                 // Method ScalaReturn$$anonfun$1."<init>":(Ljava/lang/Object;)V
        16: astore_3
        17: aload_0
        18: aload_3
        19: invokevirtual #61                 // Method func_map:(Lscala/Function1;)I
        22: goto          38
        25: astore_2
        26: aload_2
        27: invokevirtual #72                 // Method scala/runtime/NonLocalReturnControl.key:()Ljava/lang/Object;
        30: aload_1
        31: if_acmpne     39
        34: aload_2
        35: invokevirtual #75                 // Method scala/runtime/NonLocalReturnControl.value$mcI$sp:()I
        38: ireturn
        39: aload_2
        40: athrow
      Exception table:
         from    to  target type
             8    25    25   Class scala/runtime/NonLocalReturnControl
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      41     0  this   LScalaReturn$;
           17       5     3 func_Inner   Lscala/Function1;
      LineNumberTable:
        line 16: 0
        line 17: 8
        line 21: 17
        line 16: 25
      StackMapTable: number_of_entries = 3
        frame_type = 255 /* full_frame */
          offset_delta = 25
          locals = [ class ScalaReturn$, class java/lang/Object ]
          stack = [ class scala/runtime/NonLocalReturnControl ]
        frame_type = 76 /* same_locals_1_stack_item */
          stack = [ int ]
        frame_type = 252 /* append */
          offset_delta = 0
          locals = [ class scala/runtime/NonLocalReturnControl ]

從上面func2的代碼我們可以看出其捕獲了NonLocalReturnControl異常,並且判斷異常中的key是否和之前傳入進去的key一致,如果一致則返回其value,否則繼續將該異常拋出。

所以在fun2func_Inner裏,return表達式轉換成了異常拋出,在外層調用點被捕獲。這樣就實現了Scala中的Non-Loacl Return。因此在內層函數接收到參數0的時候,return的這個-1直接作爲了func2函數的返回值,而不是func_Inner的返回值。

這裏我們就要問了,在什麼情況下return會被解釋成拋出異常呢?,那就是return出現在非命名閉包裏的時候,比如我們常見的lambda表達式裏。一旦這些地方出現了return,那麼它就被賦予了退出其所在的外層函數的使命,如果一直到不了外層函數並且未被捕獲,那麼它可能會終止你的線程。

後記:
我注意到return這個東西的原因是之前在我做一個項目的時候,我使用了Scala作爲框架開發語言,在進程間交互的時候我使用了一個定時器線程來輪詢另外一個接口的狀態,但是偶爾我會發現這個定時器線程莫名其妙地就沒有了,在日誌裏也看不到什麼異常。之後我對定時器內的方法加上了try catch Throwable,並且把異常日誌打印出來。這時候我發現了一個奇怪的異常,也就是上面說的NonLocalReturnControl,當時我也沒搞清楚這是啥東西,直到最近看博客的時候才發現Scala裏的return原來還有另一種語義。這纔去回看之前的代碼,發現確實在lambda表達式裏寫了return語句,原本以爲只會向Java裏一樣退出這個lambda表達式,結果它時不時就把我的線程給殺掉了。

關於Non-local return的解釋可以參考以下鏈接,解釋的很詳細:
https://www.zhihu.com/question/22240354/answer/64673094

stackoverflow中的討論:
https://stackoverflow.com/questions/12560463/return-in-scala

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章