1. 程式人生 > >【Scala】Scala中的模式匹配、型別引數與隱式轉換

【Scala】Scala中的模式匹配、型別引數與隱式轉換

1、模式匹配

(1)概述
模式匹配是Scala中非常有特色,非常強大的一種功能。模式匹配,其實類似於Java中的swich case語法,即對一個值進行條件判斷,然後針對不同的條件,進行不同的處理。但是Scala的模式匹配的功能比Java的swich case語法的功能要強大得多,Java的swich case語法只能對值進行匹配。但是Scala的模式匹配除了可以對值進行匹配之外,還可以對型別進行匹配、對Array和List的元素情況進行匹配、對case class進行匹配、甚至對有值或沒值(Option)進行匹配。而且對於Spark來說,Scala的模式匹配功能也是極其重要的,在spark原始碼中大量地使用了模式匹配功能。因此為了更好地編寫Scala程式,並且更加通暢地看懂Spark的原始碼,學好模式匹配都是非常重要的。
(2)基本語法
Scala是沒有Java中的switch case語法的,相對應的,Scala提供了更加強大的match case語法,即模式匹配,類替代switch case,match case也被稱為模式匹配。
Scala的match case與Java的switch case最大的不同點在於,Java的switch case僅能匹配變數的值,比1、2、3等;而Scala的match case可以匹配各種情況,比如變數的型別、集合的元素、有值或無值。
match case的語法如下:變數 match { case 值 => 程式碼 }。如果值為下劃線,則代表了不滿足以上所有情況下的預設情況如何處理。此外,match case中,只要一個case分支滿足並處理了,就不會繼續判斷下一個case分支了。(與Java不同,java的switch case需要用break阻止)。match case語法最基本的應用,就是對變數的值進行模式匹配。

案例:成績評價:

def judgeGrade(grade: String) {
  grade match {
    case "A" => println("Excellent")
    case "B" => println("Good")
    case "C" => println("Just so so")
    case _ => println("you need work harder")
  }
}

(3)在模式匹配中使用if守衛
Scala的模式匹配語法,有一個特點在於,可以在case後的條件判斷中,不僅僅只是提供一個值,而是可以在值後面再加一個if守衛,進行雙重過濾。

案例:成績評價(升級版):

def judgeGrade(name: String, grade: String) {
  grade match {
    case "A" => println(name + ", you are excellent")
    case "B" => println(name + ", you are good")
    case "C" => println(name + ", you are just so so")
    case _ if name == "leo" => println(name + ", you are a good boy, come on")
    case _ => println("you need to work harder")
  }
}

(4)在模式匹配中進行變數賦值
Scala的模式匹配語法,有一個特點在於,可以將模式匹配的預設情況,下劃線,替換為一個變數名,此時模式匹配語法就會將要匹配的值賦值給這個變數,從而可以在後面的處理語句中使用要匹配的值。
為什麼有這種語法?思考一下。因為只要使用用case匹配到的值,是不是我們就知道這個只啦!!在這個case的處理語句中,是不是就直接可以使用寫程式時就已知的值!但是對於下劃線_這種情況,所有不滿足前面的case的值,都會進入_這種預設情況進行處理,此時如果我們在處理語句中需要拿到具體的值進行處理呢?那就需要使用這種在模式匹配中進行變數賦值的語法!

案例:成績評價(升級版):

def judgeGrade(name: String, grade: String) {
  grade match {
    case "A" => println(name + ", you are excellent")
    case "B" => println(name + ", you are good")
    case "C" => println(name + ", you are just so so")
    case _grade if name == "leo" => println(name + ", you are a good boy, come on, your grade is " + _grade)
    case _grade => println("you need to work harder, your grade is " + _grade)
  }
}

(5)對型別進行模式匹配
Scala的模式匹配一個強大之處就在於,可以直接匹配型別,而不是值!這點是java的switch case絕對做不到的。
對型別如何進行匹配?其他語法與匹配值其實是一樣的,但是匹配型別的話,就是要用“case 變數: 型別 => 程式碼”這種語法,而不是匹配值的“case 值 => 程式碼”這種語法。

案例:異常處理:

import java.io._

def processException(e: Exception) {
  e match {
    case e1: IllegalArgumentException => println("you have illegal arguments! exception is: " + e1)
    case e2: FileNotFoundException => println("cannot find the file you need read or write!, exception is: " + e2)
    case e3: IOException => println("you got an error while you were doing IO operation! exception is: " + e3)
    case _: Exception => println("cannot know which exception you have!" )
  }
}

(6)對Array和List進行模式匹配
對Array進行模式匹配,分別可以匹配帶有指定元素的陣列、帶有指定個數元素的陣列、以某元素打頭的陣列。對List進行模式匹配,與Array類似,但是需要使用List特有的::操作符。

案例:對朋友打招呼:

def greeting(arr: Array[String]) {
  arr match {
    case Array("Leo") => println("Hi, Leo!")
    case Array(girl1, girl2, girl3) => println("Hi, girls, nice to meet you. " + girl1 + " and " + girl2 + " and " + girl3)
    case Array("Leo", _*) => println("Hi, Leo, please introduce your friends to me.")
    case _ => println("hey, who are you?")
  }
}

def greeting(list: List[String]) {
  list match {
    case "Leo" :: Nil => println("Hi, Leo!")
    case girl1 :: girl2 :: girl3 :: Nil => println("Hi, girls, nice to meet you. " + girl1 + " and " + girl2 + " and " + girl3)
    case "Leo" :: tail => println("Hi, Leo, please introduce your friends to me.")
    case _ => println("hey, who are you?")
  }
}

(7)case class與模式匹配
Scala中提供了一種特殊的類,用case class進行宣告,中文也可以稱作樣例類。case class其實有點類似於Java中的JavaBean的概念。即只定義field,並且由Scala編譯時自動提供getter和setter方法,但是沒有method。case class的主建構函式接收的引數通常不需要使用var或val修飾,Scala自動就會使用val修飾(但是如果你自己使用var修飾,那麼還是會按照var來)。Scala自動為case class定義了伴生物件,也就是object,並且定義了apply()方法,該方法接收主建構函式中相同的引數,並返回case class物件。

案例:學校門禁:

class Person
case class Teacher(name: String, subject: String) extends Person
case class Student(name: String, classroom: String) extends Person

def judgeIdentify(p: Person) {
  p match {
    case Teacher(name, subject) => println("Teacher, name is " + name + ", subject is " + subject)
    case Student(name, classroom) => println("Student, name is " + name + ", classroom is " + classroom)
    case _ => println("Illegal access, please go out of the school!")
  }  
}

(8)Option與模式匹配
Scala有一種特殊的型別,叫做Option。Option有兩種值,一種是Some,表示有值,一種是None,表示沒有值。Option通常會用於模式匹配中,用於判斷某個變數是有值還是沒有值,這比null來的更加簡潔明瞭。Option的用法必須掌握,因為Spark原始碼中大量地使用了Option,比如Some(a)、None這種語法,因此必須看得懂Option模式匹配,才能夠讀懂spark原始碼。

案例:成績查詢:

val grades = Map("Leo" -> "A", "Jack" -> "B", "Jen" -> "C")
def getGrade(name: String) {
  val grade = grades.get(name)
  grade match {
    case Some(grade) => println("your grade is " + grade)
    case None => println("Sorry, your grade information is not in the system")
  }
}

2、型別引數

(1)概述
型別引數是什麼?型別引數其實就類似於Java中的泛型。先說說Java中的泛型是什麼,比如我們有List a = new ArrayList(),接著a.add(1),沒問題,a.add(“2”),然後我們a.get(1) == 2,對不對?肯定不對了,a.get(1)獲取的其實是個String:“2”,String:"2"怎麼可能與一個Integer型別的2相等呢?
所以Java中提出了泛型的概念,其實也就是型別引數的概念,此時可以用泛型建立List,List a = new ArrayListInteger,那麼,此時a.add(1)沒問題,而a.add(“2”)呢?就不行了,因為泛型會限制,只能往集合中新增Integer型別,這樣就避免了上述的問題。
那麼Scala的型別引數是什麼?其實意思與Java的泛型是一樣的,也是定義一種型別引數,比如在集合,在類,在函式中,定義型別引數,然後就可以保證使用到該型別引數的地方,就肯定,也只能是這種型別。從而實現程式更好的健壯性。
此外,型別引數是Spark原始碼中非常常見的,因此同樣必須掌握,才能看懂spark原始碼。
(2)泛型類
泛型類,顧名思義,其實就是在類的宣告中,定義一些泛型型別,然後在類內部,比如field或者method,就可以使用這些泛型型別。使用泛型類,通常是需要對類中的某些成員,比如某些field和method中的引數或變數,進行統一的型別限制,這樣可以保證程式更好的健壯性和穩定性。如果不使用泛型進行統一的型別限制,那麼在後期程式執行過程中,難免會出現問題,比如傳入了不希望的型別,導致程式出問題。在使用類的時候,比如建立類的物件,將型別引數替換為實際的型別,即可。Scala自動推斷泛型型別特性:直接給使用了泛型型別的field賦值時,Scala會自動進行型別推斷。

案例:新生報到:
每個學生來自不同的地方,id可能是Int,可能是String。

class Student[T](val localId: T) {
  def getSchoolId(hukouId: T) = "S-" + hukouId + "-" + localId
}

val leo = new Student[Int](111)

(3)泛型函式
泛型函式,與泛型類類似,可以給某個函式在宣告時指定泛型型別,然後在函式體內,多個變數或者返回值之間,就可以使用泛型型別進行宣告,從而對某個特殊的變數,或者多個變數,進行強制性的型別限制。與泛型類一樣,你可以通過給使用了泛型型別的變數傳遞值來讓Scala自動推斷泛型的實際型別,也可以在呼叫函式時,手動指定泛型型別。

案例:卡片售賣機:
可以指定卡片的內容,內容可以是String型別或Int型別。

def getCard[T](content: T) = {
  if(content.isInstanceOf[Int]) "card: 001, " + content
  else if(content.isInstanceOf[String]) "card: this is your card, " + content
  else "card: " + content
}

getCard[String]("hello world")

(4)上邊界Bounds
在指定泛型型別的時候,有時,我們需要對泛型型別的範圍進行界定,而不是可以是任意的型別。比如,我們可能要求某個泛型型別,它就必須是某個類的子類,這樣在程式中就可以放心地呼叫泛型型別繼承的父類的方法,程式才能正常的使用和執行。此時就可以使用上下邊界Bounds的特性。
Scala的上下邊界特性允許泛型型別必須是某個類的子類,或者必須是某個類的父類。

案例:在派對上交朋友:

class Person(val name: String) {
  def sayHello = println("Hello, I'm " + name)
  def makeFriends(p: Person) {
    sayHello
    p.sayHello
  }
}
class Student(name: String) extends Person(name)
class Party[T <: Person](p1: T, p2: T) {
  def play = p1.makeFriends(p2)
}

(5)下邊界Bounds
除了指定泛型型別的上邊界,還可以指定下邊界,即指定泛型型別必須是某個類的父類。

案例:領身份證:

class Father(val name: String) 
class Child(name: String) extends Father(name)

def getIDCard[R >: Child](person: R) {
  if (person.getClass == classOf[Child]) println("please tell us your parents' names.")
  else if (person.getClass == classOf[Father]) println("sign your name for your child's id card.")
  else println("sorry, you are not allowed to get id card.")
}

(6)View Bounds
上下邊界Bounds,雖然可以讓一種泛型型別,支援有父子關係的多種型別。但是,在某個類與上下邊界Bounds指定的父子類型範圍內的類都沒有任何關係,則預設是肯定不能接受的。然而,View Bounds作為一種上下邊界Bounds的加強版,支援可以對型別進行隱式轉換,將指定的型別進行隱式轉換後,再判斷是否在邊界指定的類型範圍內。

案例:跟小狗交朋友:

class Person(val name: String) {
  def sayHello = println("Hello, I'm " + name)
  def makeFriends(p: Person) {
    sayHello
    p.sayHello
  }
}
class Student(name: String) extends Person(name)
class Dog(val name: String) { def sayHello = println("Wang, Wang, I'm " + name) }

implicit def dog2person(dog: Object): Person = if(dog.isInstanceOf[Dog]) {val _dog = dog.asInstanceOf[Dog]; new Person(_dog.name) } else Nil

class Party[T <% Person](p1: T, p2: T)

(7)Context Bounds
Context Bounds是一種特殊的Bounds,它會根據泛型型別的宣告,比如“T: 型別”要求必須存在一個型別為“型別[T]”的隱式值。其實個人認為,Context Bounds之所以叫Context,是因為它基於的是一種全域性的上下文,需要使用到上下文中的隱式值以及注入。

案例:使用Scala內建的比較器比較大小:

class Calculator[T: Ordering] (val number1: T, val number2: T) {
  def max(implicit order: Ordering[T]) = if(order.compare(number1, number2) > 0) number1 else number2
}

(8)Manifest Context Bounds
在Scala中,如果要例項化一個泛型陣列,就必須使用Manifest Context Bounds。也就是說,如果陣列元素型別為T的話,需要為類或者函式定義[T: Manifest]泛型型別,這樣才能例項化Array[T]這種泛型陣列。

案例:打包飯菜(一種食品打成一包):

class Meat(val name: String)
class Vegetable(val name: String)

def packageFood[T: Manifest] (food: T*) = {
  val foodPackage = new Array[T](food.length)
  for(i <- 0 until food.length) foodPackage(i) = food(i)
  foodPackage 
}

(9)協變和逆變
Scala的協變和逆變是非常有特色的!完全解決了Java中的泛型的一大缺憾!
舉例來說,Java中,如果有Professional是Master的子類,那麼Card[Professionnal]是不是Card[Master]的子類?答案是:不是。因此對於開發程式造成了很多的麻煩。而Scala中,只要靈活使用協變和逆變,就可以解決Java泛型的問題。

案例:進入會場:

class Master
class Professional extends Master

大師以及大師級別以下的名片都可以進入會場:

class Card[+T] (val name: String)
def enterMeet(card: Card[Master]) {
  println("welcome to have this meeting!")
}

只要專家級別的名片就可以進入會場,如果大師級別的過來了,當然可以了!

class Card[-T] (val name: String)
def enterMeet(card: Card[Professional]) {
  println("welcome to have this meeting!")
}

(10)Existential Type
在Scala裡,有一種特殊的型別引數,就是Existential Type,存在性型別。這種型別務必掌握是什麼意思,因為在spark原始碼實在是太常見了!

Array[T] forSome { type T }
Array[_]

3、隱式轉換

(1)概述
Scala提供的隱式轉換和隱式引數功能,是非常有特色的功能。是Java等程式語言所沒有的功能。它可以允許你手動指定,將某種型別的物件轉換成其他型別的物件。通過這些功能,可以實現非常強大,而且特殊的功能。
Scala的隱式轉換,其實最核心的就是定義隱式轉換函式,即implicit conversion function。定義的隱式轉換函式,只要在編寫的程式內引入,就會被Scala自動使用。Scala會根據隱式轉換函式的簽名,在程式中使用到隱式轉換函式接收的引數型別定義的物件時,會自動將其傳入隱式轉換函式,轉換為另外一種型別的物件並返回。這就是“隱式轉換”。
隱式轉換函式叫什麼名字是無所謂的,因為通常不會由使用者手動呼叫,而是由Scala進行呼叫。但是如果要使用隱式轉換,則需要對隱式轉換函式進行匯入。因此通常建議將隱式轉換函式的名稱命名為“one2one”的形式。
Spark原始碼中有大量的隱式轉換和隱式引數,因此必須精通這種語法。
(2)隱式轉換基本語法
要實現隱式轉換,只要程式可見的範圍內定義隱式轉換函式即可。Scala會自動使用隱式轉換函式。隱式轉換函式與普通函式唯一的語法區別就是,要以implicit開頭,而且最好要定義函式返回型別。

案例:特殊售票視窗(只接受特殊人群,比如學生、老人等):

class SpecialPerson(val name: String)
class Student(val name: String)
class Older(val name: String)

implicit def object2SpecialPerson (obj: Object): SpecialPerson = {
  if (obj.getClass == classOf[Student]) { val stu = obj.asInstanceOf[Student]; new SpecialPerson(stu.name) }
  else if (obj.getClass == classOf[Older]) { val older = obj.asInstanceOf[Older]; new SpecialPerson(older.name) }
  else Nil
}

var ticketNumber = 0
def buySpecialTicket(p: SpecialPerson) = {
  ticketNumber += 1
  "T-" + ticketNumber
}

(3)使用隱式轉換加強現有型別
隱式轉換非常強大的一個功能,就是可以在不知不覺中加強現有型別的功能。也就是說,可以為某個類定義一個加強版的類,並定義互相之間的隱式轉換,從而讓源類在使用加強版的方法時,由Scala自動進行隱式轉換為加強類,然後再呼叫該方法。

案例:超人變身:

class Man(val name: String)
class Superman(val name: String) {
  def emitLaser = println("emit a laster!")
}

implicit def man2superman(man: Man): Superman = new Superman(man.name)

val leo = new Man("leo")
leo.emitLaser

(4)隱式轉換函式作用域與匯入
Scala預設會使用兩種隱式轉換,一種是源型別,或者目標型別的伴生物件內的隱式轉換函式;一種是當前程式作用域內的可以用唯一識別符號表示的隱式轉換函式。
如果隱式轉換函式不在上述兩種情況下的話,那麼就必須手動使用import語法引入某個包下的隱式轉換函式,比如import test._。通常建議,僅僅在需要進行隱式轉換的地方,比如某個函式或者方法內,用iimport匯入隱式轉換函式,這樣可以縮小隱式轉換函式的作用域,避免不需要的隱式轉換。
(5)隱式轉換的發生時機

  1. 呼叫某個函式,但是給函式傳入的引數的型別,與函式定義的接收引數型別不匹配(案例:特殊售票視窗)
  2. 使用某個型別的物件,呼叫某個方法,而這個方法並不存在於該型別時(案例:超人變身)
  3. 使用某個型別的物件,呼叫某個方法,雖然該型別有這個方法,但是給方法傳入的引數型別,與方法定義的接收引數的型別不匹配(案例:特殊售票視窗加強版)

案例:特殊售票視窗加強版:

class TicketHouse {
  var ticketNumber = 0
  def buySpecialTicket(p: SpecialPerson) = {
    ticketNumber += 1
    "T-" + ticketNumber
  }
}

(6)隱式引數
所謂的隱式引數,指的是在函式或者方法中,定義一個用implicit修飾的引數,此時Scala會嘗試找到一個指定型別的,用implicit修飾的物件,即隱式值,並注入引數。
Scala會在兩個範圍內查詢:一種是當前作用域內可見的val或var定義的隱式變數;一種是隱式引數型別的伴生物件內的隱式值。

案例:考試簽到:

class SignPen {
  def write(content: String) = println(content)
}
implicit val signPen = new SignPen

def signForExam(name: String) (implicit signPen: SignPen) {
  signPen.write(name + " come to exam in time.")
}