scala語法 - 方法與函式

止魚發表於2018-10-15

Scala 有方法與函式,二者在語義上的區別很小。Scala 方法是類的一部分,而函式是一個物件可以賦值給一個變數。換句話來說在類中定義的函式即是方法。

Scala 中的方法跟 Java 的類似,方法是組成類的一部分。

Scala 中的函式則是一個完整的物件,Scala 中的函式其實就是繼承了 Trait 的類的物件。

Scala 中使用 val 語句可以定義函式,def 語句定義方法和函式。

class Test {
    def method (x: Int) = x + 3
    val function = (x: Int) => x + 3
}

方法

方法宣告

def methodName([引數列表]) : [返回型別]

如果不寫 = {},等於號和方法主體,那麼該方法會被隱式宣告為抽象(abstract),包含他的型別也是也是一個抽象型別

方法定義

方法定義由一個 def 關鍵字開始,緊接著是可選的引數列表,一個冒號 : 和方法的返回型別,一個等於號 = ,最後是方法的主體。

Scala 方法定義格式如下:

def functionName ([引數列表]) : [return type] = {
   function body
   return [expr]
}

以上程式碼中 return type 可以是任意合法的 Scala 資料型別。引數列表中的引數可以使用逗號分隔。

以下方法的功能是將兩個傳入的引數相加並求和:

object add{
   def addInt( a:Int, b:Int ) : Int = {
      var sum:Int = 0
      sum = a + b

      return sum
   }
}

如果方法沒有返回值,可以返回為 Unit,這個類似於 Java 的 void, 例項如下:

object Hello{
   def printMe( ) : Unit = {
      println("Hello, Scala!")
   }
}

方法呼叫

Scala 提供了多種不同的方法呼叫方式,以下是呼叫方法的標準格式:

functionName( 引數列表 )

如果方法使用了例項的物件來呼叫,我們可以使用類似java的格式 (使用 . 號):

[instance.]functionName( 引數列表 )

以上例項演示了定義與呼叫方法的例項:

object Test {
   def main(args: Array[String]) {
        println( "Returned Value : " + addInt(5,7) );
   }
   def addInt( a:Int, b:Int ) : Int = {
      var sum:Int = 0
      sum = a + b

      return sum
   }
}

執行以上程式碼,輸出結果為:

$ scalac Test.scala 
$ scala Test
Returned Value : 12

在scala中,如果class A需要呼叫 class B中的方法 methodA, 區別於Java,class B不需要new例項化

B.methodA

函式

函式與方法大致一樣,但在scala中,函式的概念更為重要與廣泛,並且更為靈活

定義一個函式

類似方法的定義規則:

def abs (x: Double) = if (x > 0) x else -x

你必須給出所有引數的型別,在函式不是遞迴函式的情況下,scala可以通過=後面的表示式推斷出返回值型別。但是,一旦函式是遞迴的,那麼你也需要顯示指定返回值型別:

def fac (n: Int): Int = if (n < 0) 1 else n * fac(n-1)

在諸如 ML、Haskell中,語法能夠推斷出遞迴函式的型別,使用的是 hindley-milner演算法

匿名函式

Scala 中定義匿名函式的語法很簡單,箭頭左邊是引數列表,右邊是函式體。
使用匿名函式後,我們的程式碼變得更簡潔了。

下面的表示式就定義了一個接受一個Int型別輸入引數的匿名函式:

var inc = (x:Int) => x+1

上述定義的匿名函式,其實是下面這種寫法的簡寫:

def add2 = new Function1[Int,Int]{  
    def apply(x:Int):Int = x+1;  
} 

以上例項的 inc 現在可作為一個函式,使用方式如下:

var x = inc(7)-1

同樣我們可以在匿名函式中定義多個引數:

var mul = (x: Int, y: Int) => x*y

mul 現在可作為一個函式,使用方式如下:

println(mul(3, 4))

我們也可以不給匿名函式設定引數,如下所示:

var userDir = () => { System.getProperty("user.dir") }

userDir 現在可作為一個函式,使用方式如下:

println( userDir() )

例項

object Demo {
   def main(args: Array[String]) {
      println( "multiplier(1) value = " +  multiplier(1) )
      println( "multiplier(2) value = " +  multiplier(2) )
   }
   var factor = 3
   val multiplier = (i:Int) => i * factor
}

將以上程式碼保持到 Demo.scala 檔案中,執行以下命令:

$ scalac Demo.scala
$ scala Demo

輸出結果為:

multiplier(1) value = 3
multiplier(2) value = 6

函式傳名呼叫(call-by-name)

Scala的直譯器在解析函式引數(function arguments)時有兩種方式:

  • 傳值呼叫(call-by-value):先計算參數列達式的值,再應用到函式內部;
  • 傳名呼叫(call-by-name):將未計算的參數列達式直接應用到函式內部

在進入函式內部前,傳值呼叫方式就已經將參數列達式的值計算完畢,而傳名呼叫是在函式內部進行參數列達式的值計算的。

這就造成了一種現象,每次使用傳名呼叫時,直譯器都會計算一次表示式的值。

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

   def time() = {
      println("獲取時間,單位為納秒")
      System.nanoTime
   }
   def delayed( t: => Long ) = {
      println("在 delayed 方法內")
      println("引數: " + t)
      t
   }
}

以上例項中我們宣告瞭 delayed 方法, 該方法在變數名和變數型別使用 => 符號來設定傳名呼叫。執行以上程式碼,輸出結果如下:

$ scalac Test.scala 
$ scala Test
在 delayed 方法內
獲取時間,單位為納秒
引數: 241550840475831
獲取時間,單位為納秒

例項中 delay 方法列印了一條資訊表示進入了該方法,接著 delay 方法列印接收到的值,最後再返回 t。

可變引數

Scala 允許你指明函式的最後一個引數可以是重複的,即我們不需要指定函式引數的個數,可以向函式傳入可變長度引數列表。

Scala 通過在引數的型別之後放一個星號來設定可變引數(可重複的引數)。例如:

object Test {
   def main(args: Array[String]) {
        printStrings("Runoob", "Scala", "Python");
   }
   def printStrings( args:String* ) = {
      var i : Int = 0;
      for( arg <- args ){
         println("Arg value[" + i + "] = " + arg );
         i = i + 1;
      }
   }
}

執行以上程式碼,輸出結果為:

$ scalac Test.scala
$ scala Test
Arg value[0] = Runoob
Arg value[1] = Scala
Arg value[2] = Python

預設引數

Scala 可以為函式引數指定預設引數值,使用了預設引數,你在呼叫函式的過程中可以不需要傳遞引數,這時函式就會呼叫它的預設引數值,如果傳遞了引數,則傳遞值會取代預設值。例項如下:

object Test {
   def main(args: Array[String]) {
        println( "返回值 : " + addInt() );
   }
   def addInt( a:Int=5, b:Int=7 ) : Int = {
      var sum:Int = 0
      sum = a + b

      return sum
   }
}

執行以上程式碼,輸出結果為:

$ scalac Test.scala
$ scala Test
返回值 : 12

函式巢狀

我們可以在 Scala 函式內定義函式,定義在函式內的函式稱之為區域性函式。

以下例項我們實現階乘運算,並使用內嵌函式:

object Test {
   def main(args: Array[String]) {
      println( factorial(0) )
      println( factorial(1) )
      println( factorial(2) )
      println( factorial(3) )
   }

   def factorial(i: Int): Int = {
      def fact(i: Int, accumulator: Int): Int = {
         if (i <= 1)
            accumulator
         else
            fact(i - 1, i * accumulator)
      }
      fact(i, 1)
   }
}

執行以上程式碼,輸出結果為:

$ scalac Test.scala
$ scala Test
1
1
2
6

偏應用函式

Scala 偏應用函式是一種表示式,你不需要提供函式需要的所有引數,只需要提供部分,或不提供所需引數。

如下例項,我們列印日誌資訊:

import java.util.Date

object Test {
   def main(args: Array[String]) {
      val date = new Date
      log(date, "message1" )
      Thread.sleep(1000)
      log(date, "message2" )
      Thread.sleep(1000)
      log(date, "message3" )
   }

   def log(date: Date, message: String)  = {
     println(date + "----" + message)
   }
}

執行以上程式碼,輸出結果為:

$ scalac Test.scala
$ scala Test
Mon Dec 02 12:52:41 CST 2013----message1
Mon Dec 02 12:52:41 CST 2013----message2
Mon Dec 02 12:52:41 CST 2013----message3

例項中,log() 方法接收兩個引數:date 和 message。我們在程式執行時呼叫了三次,引數 date 值都相同,message 不同。

我們可以使用偏應用函式優化以上方法,繫結第一個 date 引數,第二個引數使用下劃線(_)替換缺失的引數列表,並把這個新的函式值的索引的賦給變數。以上例項修改如下:

import java.util.Date

object Test {
   def main(args: Array[String]) {
      val date = new Date
      val logWithDateBound = log(date, _ : String)

      logWithDateBound("message1" )
      Thread.sleep(1000)
      logWithDateBound("message2" )
      Thread.sleep(1000)
      logWithDateBound("message3" )
   }

   def log(date: Date, message: String)  = {
     println(date + "----" + message)
   }
}

執行以上程式碼,輸出結果為:

$ scalac Test.scala
$ scala Test
Mon Dec 02 12:53:56 CST 2013----message1
Mon Dec 02 12:53:56 CST 2013----message2
Mon Dec 02 12:53:56 CST 2013----message3

指定函式引數名

一般情況下函式呼叫引數,就按照函式定義時的引數順序一個個傳遞。但是我們也可以通過指定函式引數名,並且不需要按照順序向函式傳遞引數,例項如下:

object Test {
   def main(args: Array[String]) {
        printInt(b=5, a=7);
   }
   def printInt( a:Int, b:Int ) = {
      println("Value of a : " + a );
      println("Value of b : " + b );
   }
}

執行以上程式碼,輸出結果為:

$ scalac Test.scala
$ scala Test
Value of a :  7
Value of b :  5

高階函式

高階函式(Higher-Order Function)就是操作其他函式的函式。

Scala 中允許使用高階函式, 高階函式可以使用其他函式作為引數,或者使用函式作為輸出結果。

以下例項中,apply() 函式使用了另外一個函式 f 和 值 v 作為引數,而函式 f 又呼叫了引數 v:

object Test {
   def main(args: Array[String]) {

      println( apply( layout, 10) )

   }
   // 函式 f 和 值 v 作為引數,而函式 f 又呼叫了引數 v
   def apply(f: Int => String, v: Int) = f(v)

   def layout[A](x: A) = "[" + x.toString() + "]"
   
}

執行以上程式碼,輸出結果為:

$ scalac Test.scala
$ scala Test
[10]

函式柯里化(Currying)

柯里化(Currying)指的是將原來接受兩個引數的函式變成新的接受一個引數的函式的過程。新的函式返回一個以原有第二個引數為引數的函式。

例項:
首先我們定義一個函式:

def add(x:Int,y:Int)=x+y

那麼我們應用的時候,應該是這樣用:add(1,2)

現在我們把這個函式變一下形:

def add(x:Int)(y:Int) = x + y

那麼我們應用的時候,應該是這樣用:add(1)(2),最後結果都一樣是3,這種方式(過程)就叫柯里化。

實現過程
add(1)(2) 實際上是依次呼叫兩個普通函式(非柯里化函式),第一次呼叫使用一個引數 x,返回一個函式型別的值,第二次使用引數y呼叫這個函式型別的值。

實質上最先演變成這樣一個方法:

def add(x:Int)=(y:Int)=>x+y

那麼這個函式是什麼意思呢? 接收一個x為引數,返回一個匿名函式,該匿名函式的定義是:接收一個Int型引數y,函式體為x+y。現在我們來對這個方法進行呼叫。

val result = add(1) 

返回一個result,那result的值應該是一個匿名函式:(y:Int)=>1+y

所以為了得到結果,我們繼續呼叫result。

val sum = result(2)

最後列印出來的結果就是3。

完整例項
下面是一個完整例項:

object Test {
   def main(args: Array[String]) {
      val str1:String = "Hello, "
      val str2:String = "Scala!"
      println( "str1 + str2 = " +  strcat(str1)(str2) )
   }

   def strcat(s1: String)(s2: String) = {
      s1 + s2
   }
}

執行以上程式碼,輸出結果為:

$ scalac Test.scala
$ scala Test
str1 + str2 = Hello, Scala!

相關文章