五、Scala從入門到精通一一函數式編程(基礎)

1、 函數式編程內容及講課順序

1.1、函數式編程內容

函數式編程基礎
1、函數定義/聲明
2、函數運行機制
3、遞歸 // 難點 [最短路徑,郵差問題,迷宮問題]
4、過程
5、惰性函數和異常
函數式編程高級
1、值函數(函數字面量)
2、閉包
3、應用函數
4、抽象控制…

1.2、函數式編程授課順序

1、在scala中,函數式編程和麪向對象編程融合在一起,學習函數式編程式需要oop的知識,同樣學習oop需要函數式編程的基礎。
2、關係如下圖:
在這裏插入圖片描述
3、授課順序: 函數式編程基礎->面向對象編程->函數式編程高級

2、函數式編程內容

2.1、幾個概念的說明

在學習Scala中將方法、函數、函數式編程和麪向對象編程明確一下:

1、在scala中,方法函數幾乎可以等同(比如他們的定義、使用、運行機制都一樣的),只是函數的使用方式更加的靈活多樣 [方法轉函數]。
2、函數式編程是從編程方式(範式)的角度來談的,可以這樣理解:函數式編程把函數當做一等公民,充分利用函數、 支持的函數的多種使用方式
比如:
在Scala當中,函數是一等公民,像變量一樣,既可以作爲函數的參數使用,也可以將函數賦值給一個變量. ,函數的創建不用依賴於類或者對象,而在Java當中,函數的創建則要依賴於類、抽象類或者接口.
3、面向對象編程是以對象爲基礎的編程方式。
4、在scala中函數式編程和麪向對象編程融合在一起了 。

2.2、在學習Scala中將方法、函數、函數式編程和麪向對象編程關係分析圖

在這裏插入圖片描述

2.3、函數式編程小結

1、“函數式編程"是一種"編程範式”(programming paradigm)。
2、它屬於"結構化編程"的一種,主要思想是把運算過程儘量寫成一系列嵌套的函數調用。
3、函數式編程中,將函數也當做數據類型,因此可以接受函數當作輸入(參數)和輸出(返回值)。(增強了編程的粒度)
4、函數式編程中,最重要的就是函數。

3、爲什麼需要函數

在這裏插入圖片描述

4、函數的定義

4.1、基本語法

def 函數名 ([參數名: 參數類型], …)[[: 返回值類型] =] {
語句… //完成某個功能
return 返回值
}
1、函數聲明關鍵字爲def (definition)
2、[參數名: 參數類型], …:表示函數的輸入(就是參數列表), 可以沒有。 如果有,多個參數使用逗號間隔
3、函數中的語句:表示爲了實現某一功能代碼塊
4、函數可以有返回值,也可以沒有
5、返回值形式1: // def 函數名(參數列表) : 數據類型 = {函數體} // 返回值確定,清晰
6、返回值形式2: // def 函數名(參數列表) = {函數體} // 有返回值, 類型是推斷出來的
7、返回值形式3: // def 函數名(參數列表) {函數體} // 無返回值 Unit
8、如果沒有return ,默認以執行到最後一行的結果作爲返回值

4.2、快速入門案例

def main(args: Array[String]): Unit = {
  val n1 = 10
  val n2 = 20
  println("res=" + getRes(1, 2, '+'))
}
// 定義函數/方法
def getRes(n1: Int, n2: Int, oper: Char) = {
  if (oper == '+') {
    n1 + n2 // 返回
  } else if (oper == '-') {
    n1 - n2
  } else {
    // 返回null
    null
  }
}

5、函數-調用機制

5.1、函數-調用過程

1、爲了讓大家更好的理解函數調用機制,看一個案例,並畫出示意圖,這個很重要,比如getSum計算兩個數的和,並返回結果
在這裏插入圖片描述
一個函數/方法在函數/方法體內又調用了本身,我們稱爲遞歸調用在這裏插入圖片描述
當調用test(4) 和 test2(4) 上面兩段代碼分別輸出什麼?
遞歸調用並分析原因
在這裏插入圖片描述

5.2、函數遞歸調用的重要的規則和小結

1、程序執行一個函數時,就創建一個新的受保護的獨立空間(新函數棧楨)
2、函數的局部變量是獨立的,不會相互影響
3、遞歸必須向退出遞歸的條件逼近,否則就是無限遞歸,會造成死循環
4、當一個函數執行完畢,或者遇到return,就會返回,遵守誰調用,就將結果返回給誰。

5.3、使用Scala遞歸調用的應用案例

1、斐波那契數
使用遞歸的方式,求出斐波那契數1,1,2,3,5,8,13…
給你一個整數n,求出它的斐波那契數是多少?

println("fbn的結果是:" + fbn(7))
 def fbn(n: Int): Int = {

    // 分析
    // 1、當n = 1 結果爲1
    // 2、當n = 2 結果爲1
    // 3、當n>2 時 結果就是前面兩個數的和
    if (n == 1 || n == 2) {
      1
    } else {
      fbn(n - 1) + fbn(n - 2)
    }
  }

2、求函數值
已知f(1) = 3 ; f(n)= 2 * f(n-1)+1
請使用遞歸的思想變成,求出f(n)的值?

  println(f(2))
 def f(n: Int): Int = {
    if (n == 1) {
      3
    } else {
      2 * f(n - 1) + 1
    }
  }

3、猴子喫桃子問題
有一堆桃子,猴子第一天吃了其中的一半,並再多吃了一個!以後每天猴子都喫其中的一半,然後再多喫一個,當到第10天時,想再喫時(還沒喫),發現只有一個桃子了。問題:最初有多少個桃子

    思路分析:
    1 day =10 桃子有 1
    2 day = 9 桃子有 2*(day10[1] + 1)
    3 day = 8 桃子有 (day9[4] + 1) * 2
	println("最初有多少個桃子:" + peach(1))
      def peach(day: Int): Int = {
    if (day == 10) {
      1
    } else {
      (peach(day + 1) + 1) * 2
    }
  }

5.4、函數注意事項和細節討論

1、函數的形參列表可以是多個, 如果函數沒有形參,調用時 可以不帶()
2、形參列表和返回值的數據類型可以是值類型和引用類型。

object Details01 {
  def main(args: Array[String]): Unit = {
    // 形參列表和返回值列表的數據類型可以是值類型和引用類型
    val tiger = new Tiger
    val tiger2 = test01(10, tiger)
    println(tiger2.name) // jack
    println(tiger.name)
    println(tiger.hashCode()+" "+tiger2.hashCode())
  }
  def test01(n1: Int, tiger: Tiger): Tiger = {
    println("n1=" + n1)
    tiger.name = "jack"
    tiger
  }
}
class Tiger {
  // 一個名字屬性
  var name = ""
}

3、Scala中的函數可以根據函數體最後一行代碼自行推斷函數返回值類型。那麼在這種情況下,return關鍵字可以省略
在這裏插入圖片描述
4、因爲Scala可以自行推斷,所以在省略return關鍵字的場合,返回值類型也可以省略
在這裏插入圖片描述
5、如果函數明確使用return關鍵字,那麼函數返回就不能使用自行推斷了,這時要明確寫成 : 返回類型 = ,當然如果你什麼都不寫,即使有return 返回值爲()

def main(args: Array[String]): Unit = {

  println(getSum2(10, 3)) // ()
}
// 如果寫了return,返回值類型就不能省略
def getSum(n1: Int,n2:Int):Int = {
  return n1 + n2
}
// 如果返回值這裏什麼都沒有寫,即表示該函數沒有返回值
// 這時return 無效
def getSum2(n1: Int,n2:Int) {
  return n1 + n2
}

6、如果函數明確聲明無返回值(聲明Unit),那麼函數體中即使使用return關鍵字也不會有返回值

println(GetSum(10,30))
// 如果函數明確聲明無返回值(聲明爲Unit),
// 那麼函數體中即使使用return關鍵字也不會有返回值
def GetSum(n1:Int,n2:Int):Unit={
  return n1 + n2
}

7、如果明確函數無返回值或不確定返回值類型,那麼返回值類型可以省略(或聲明爲Any)在這裏插入圖片描述
8、Scala語法中任何的語法結構都可以嵌套其他語法結構(靈活),即:函數/方法中可以再聲明/定義函數/方法,類中可以再聲明類

def main(args: Array[String]): Unit = {
  def f1(): Unit = { // ok
    println("f1")
  }

  println("ok~~")
  def SayOk(): Unit = { // private final sayOk$1()
  println("main sayok")

  def SayOk(): Unit = { // private final sayOk$2()
    println("main sayok")
      }
    }
}

def SayOk(): Unit = { // 成員
  println("main sayok")
}

在這裏插入圖片描述
在這裏插入圖片描述
9、Scala函數的形參,在聲明函數時,直接賦初始值(默認值),這時調用函數時,如果沒有指定實參,則會使用默認值。如果指定了實參,則實參會覆蓋默認值

def main(args: Array[String]): Unit = {
  println(sayOk("mary"))
}
// name形參的默認值爲jack
def sayOk(name: String = "jack"): String = {
  return name +" "+ "ok!"
}

10、如果函數存在多個參數,每個參數都可以設定默認值,那麼這個時候,傳遞的參數到底是覆蓋默認值,還是賦值給沒有默認值的參數,就不確定了(默認按照聲明順序[從左到右])。在這種情況下,可以採用帶名參數[案列演示+練習]

def main(args: Array[String]): Unit = {
  //mysqlCon()
  //mysqlCon("127.0.0.1",7777) // 從左到右覆蓋

  // 如果我們希望指定覆蓋某個默認值,則使用帶名參數即可,
  // 比如修改用戶名和密碼
  mysqlCon(user = "tom", pwd = "123")

  // f6("v2") // x
  f6(p2 = "v2") // √
}


def mysqlCon(add: String = "localhost", post: Int = 3306,
             user: String = "root", pwd: String = "root"
            ): Unit = {
  println("add=" + add)
  println("post=" + post)
  println("user=" + user)
  println("pwd=" + pwd)
}

def f6(p1: String = "v1", p2: String) {
  println(p1 + p2)
}

11、Scala 函數的形參默認是val的 ,因此不能再函數中修改
12、遞歸函數爲執行之前是無法推斷出來結果類型,在使用時必須有明確的返回值類型

def f8(n:Int)={ //? 錯誤,遞歸不能使用類型推斷,必須指定返回的數據類型
    if(n <= 0)
    1
    else
    n * f8(n - 1)
}

13、Scala 函數支持可變參數
// 支持0到多個參數
def sum(args: Int) : Int = {
}
// 支持1到多個參數
def sum(n1:Int,args: Int*) : Int = {
}
說明:
1、args是集合,通過for循環可以訪問到各個值
2、案列演示,編寫一個函數sum,可以求出1 到 多個Int的和
3、可變參數需要寫在形參列表的最後。
在這裏插入圖片描述

 // 編寫一個函數sum,可以求出1 到 多個Int的和
  println(sum(10, 30, 40, 3, 45, 7))
}

//
def sum(n1: Int, args: Int*): Int = {
  println("args.length:" + args.length)
  // 遍歷
  var sum = n1
  for (item <- args) {
    sum += item
  }
  sum
}

/*// 可變參數需要放在最後
def sum2( args: Int*,n1: Int): Int =*/

5.5、函數練習題

object Hello01 {
    def main(args: Array[String]): Unit = {
        def f1 = "venassa" 
        println(f1) //  venassa
    }
}

6、過程

6.1、基本概念

將函數的返回類型爲Unit的函數稱之爲過程(procedure),如果明確沒有返回值,那麼等號可以省略
案例說明:

// f10 沒有返回值,可以使用Unit來說明
// 這時,這個函數我們也叫過程(procedure)
def f10(name:String) : Unit = {
    println(name + "Hello")
}

6.2、注意事項

1)注意區分:如果函數聲明時沒有返回值類型,但是有=號,可以進行類型推斷最後一行代碼。這時這個函數實際是沒有返回值的,該函數並不是過程。(把=號去掉的)
2)開發工具的自動代碼補全功能,雖然會自動加上Unit,但是考慮到Scala語言的簡單,靈活,最好不加

7、惰性函數

7.1、看一個應用場景

惰性計算(儘可能延遲表達式求值)是許多函數式編程語言的特性。惰性集合在需要時提供其元素,無需預先計算它們,這帶來了一些好處。首先,您可以將耗時的計算推遲到絕對需要的時候。其次,您可以創造無限個集合,只要它們繼續收到請求,就會繼續提供元素。函數的惰性使用讓您能夠得到更高效的代碼。Java 並沒有爲惰性提供原生支持,Scala提供了。

7.2、畫圖說明[大數據推薦系統]

在這裏插入圖片描述

7.3、Scala中實現懶加載的代碼

當函數返回值被聲明爲lazy時,函數的執行將被推遲,直到我們首次對此取值,該函數纔會執行,這種函數我們稱之爲惰性函數,在Java 的某些框架代碼中稱爲懶加載(延遲加載)

def main(args: Array[String]): Unit = {
  lazy val res = sum(10, 20)
  println("··················")
  println("res = " + res) // 在要使用res前,才執行
}
// sum函數,返回和
def sum(n1: Int, n2: Int) = {
  println("sum()執行了") // 輸出一句話
  n1 + n2
}

7.4、注意事項和細節說明:

1、lazy不能修飾var類型的變量
在這裏插入圖片描述
2、不但是 在調用函數時,加了 lazy ,會導致函數的執行被推遲,我們在聲明一個變量時,如果給聲明瞭 lazy ,那麼變量值得分配也會推遲。 比如 lazy val i = 10
在這裏插入圖片描述

8、異常處理

8.1、介紹

1、Scala提供try和catch塊來處理異常。try塊用於包含可能出錯的代碼。catch塊用於處理try塊中發生的異常。可以根據需要在程序中有任意數量的try…catch塊。
2、語法處理上和Java類似,但是又不盡相同

8.2、Java異常處理回顧

public class JavaExceptionDemo01 {
    public static void main(String[] args) {
        try {
            // 可疑代碼
            int i = 0;
            int b = 10;
            int c = b / i; //執行代碼時,會拋出ArithmeticException異常

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 最終執行的代碼
            System.out.println("java Finally");
        }
        System.out.println("ok···繼續執行");
    }
}

8.3、 Java異常處理的注意點

1、Java語言按照try-catch-catch…–finally的方式來處理異常
2、不管有沒有異常捕獲,都會執行finally,因此通常可以在finally代碼塊中釋放資源
3、可以有多個catch,分別捕獲對應的異常,這時需要把範圍小的異常類寫在前面,把範圍大的異常類寫在後面,否則編譯錯誤。會提示“Exception ‘java.lang.xxxxx’has already been caught "[案列演示]
在這裏插入圖片描述

8.4、Scala異常處理舉例

object ScalaExceptionDemo {
  def main(args: Array[String]): Unit = {
    try {
      val r = 10 / 0
    } catch {
      // 說明
      // 1.在Scala中只有一個catch
      // 2.在catch中有多個case,每個case可以匹配一種異常
      // 3. => 關鍵符號,表示後面是對該異常的處理代碼塊
      // 4. finally 最終要執行的
      case ex: ArithmeticException => {
        println("捕獲了除數爲0的算術異常")
      }
      case ex: Exception => println("捕獲了異常")
    } finally {
      // 最終要執行的代碼
      println("Scala finally")
    }
    println("ok···繼續執行")
  }
}

8.5、Scala異常處理小結

1、我們將可疑代碼封裝在try塊中。 在try塊之後使用了一個catch處理程序來捕獲異常。如果發生任何異常,catch處理程序將處理它,程序將不會異常終止。
2、Scala的異常的工作機制和Java一樣,但是Scala沒有“checked(編譯期)”異常,即Scala沒有編譯異常這個概念,異常都是在運行的時候捕獲處理。
3、用throw關鍵字,拋出一個異常對象。所有異常都是Throwable的子類型。throw表達式是有類型的,就是Nothing,因爲Nothing是所有類型的子類型,所以throw表達式可以用在需要類型的地方
在這裏插入圖片描述
4、在Scala裏,借用了模式匹配的思想來做異常的匹配,因此,在catch的代碼裏,是一系列case子句來匹配異常。【前面案例可以看出這個特點, 模式匹配我們後面詳解】,當匹配上後 => 有多條語句可以換行寫,類似 java 的 switch case x: 代碼塊…
5、異常捕捉的機制與其他語言中一樣,如果有異常發生,catch子句是按次序捕捉的。因此,在catch子句中,越具體的異常越要靠前,越普遍的異常越靠後,如果把越普遍的異常寫在前,把具體的異常寫在後,在scala中也不會報錯,但這樣是非常不好的編程風格。
6、finally子句用於執行不管是正常處理還是有異常發生時都需要執行的步驟,一般用於對象的清理工作,這點和Java一樣。
7、Scala提供了throws關鍵字來聲明異常。可以使用方法定義聲明異常。 它向調用者函數提供了此方法可能引發此異常的信息。 它有助於調用函數處理並將該代碼包含在try-catch塊中,以避免程序異常終止。在scala中,可以使用throws註釋來聲明異常
在這裏插入圖片描述

9、函數的課堂練習題

1、編寫一個函數,從終端輸入一個整數(1—9),打印出對應的乘法表:
在這裏插入圖片描述

object Exercise01 {
  def main(args: Array[String]): Unit = {
    printf("請輸入一個數字(1~9)之間:")
    val n = StdIn.readInt()
    math(n)
  }
  // 編寫一個函數,輸出99乘法表
  def math(n: Int) = {
    for (i <- 1 to n) {
      for (j <- 1 to i) {
        printf("%d * %d = %d\t", j, i, i * j)
      }
      println()
    }
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章