強大的Scala模式匹配

用過Scala的模式匹配,感受Java的弱爆了。Scala幾乎能夠匹配任何數據類型,若是默認的不能知足你的要求,你能夠自定義模式匹配。html

介紹Scala的模式匹配前,咱們先了解清楚unapply()與unapplySeq()兩個方法:java

名字叫作unapply和unapplySeq的方法在Scala裏也是有特殊含義的。json

咱們前面說過case class在作pattern match時很是好用,而除case class以外,有unapply或unapplySeq方法的對象在pattern match時也有很是好的應用場景。app

比方這段代碼:post

1 2 3 
object Square {  def unapply(z: Double): Option[Double] = Some(math.sqrt(z)) } 

咱們定義了一個unapply方法,用來計算平方根。url

咱們可以像調用普通方法同樣的調用它:spa

1 2 
val number: Double = 36.0 Square.unapply(number) 

這樣會獲得36的平方根:6。實際上返回值是Some(6)。scala

上面的方式是對unapply的浪費。unapply真正的優勢是這種:code

1 2 3 4 5 
val number: Double = 36.0 number match {  case Square(n) => println(s"square root of $number is $n")  case _ => println("nothing matched") } 

這樣咱們無需顯式調用unapply方法,而把是它用在pattern match中。讓編譯器替咱們調用它。htm

當咱們寫下這段pattern match的代碼時,編譯器事實上替咱們作了好幾件事:

  1. 調用unapply,傳入number
  2. 接收返回值並推斷返回值是None,仍是Some
  3. 假設是Some,則將其解開,並將當中的值賦值給n(就是case Square(n)中的n)

這段代碼反編譯出來是這個樣子的:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 
 double number = 36.0D;  double d1 = number;  Option localOption = Square..MODULE$.unapply(d1);  //調用unapply,傳入number  BoxedUnit localBoxedUnit;  if (localOption.isEmpty()) {//推斷返回值是None  Predef..MODULE$.println("nothing matched");  localBoxedUnit = BoxedUnit.UNIT;  }  else {//推斷返回值是Some  double n = BoxesRunTime.unboxToDouble(localOption.get());  //將Some解開,並將當中的值賦值給n  Predef..MODULE$.println(new StringContext(Predef..MODULE$.wrapRefArray((Object[]) new String[] {  "square root of ", " is ", ""  }) ).s(Predef..MODULE$.genericWrapArray(new Object[] {  BoxesRunTime.boxToDouble(number), BoxesRunTime.boxToDouble(n)  })));  localBoxedUnit = BoxedUnit.UNIT;  } 

假設沒有unapply方法和pattern match語法之間的這樣的結合,咱們本身寫代碼要寫成什麼樣子呢?

也許會比上面反編譯的代碼簡單一些,但是顯式地調用開平方的方法。用if else來推斷Option,以及將真正的返回值從Option裏面解出來這三件事是免不掉的。

unapplySeq和unapply的做用很是是相似,好比這樣:

1 2 3 4 5 6 
object Names {  def unapplySeq(str: String): Option[Seq[String]] = {  if (str.contains(",")) Some(str.split(","))  else None  } } 

咱們定義一個unapplySeq方法,用逗號做爲分隔符來把字符串拆開。

而後咱們可以這樣應用它:

1 2 3 4 5 6 7 8 
val namesString = "xiao ming,xiao hong,tom" namesString match {  case Names(first, second, third) => {  println("the string contains three people's names")  println(s"$first $second $third")  }  case _ => println("nothing matched") } 

與上面的樣例很是是相似,只是編譯器在這裏替咱們作的事情不少其它了:

  1. 調用unapplySeq,傳入namesString
  2. 接收返回值並推斷返回值是None,仍是Some
  3. 假設是Some,則將其解開
  4. 推斷解開以後獲得的sequence中的元素的個數是不是三個
  5. 假設是三個,則把三個元素分別取出,賦值給first,second和third

假設沒有unapplySeq方法和pattern match語法之間的這樣的結合,咱們本身寫代碼來作這五件事會顯得很是是繁瑣。

 

你們瞭解清楚unapply()與unapplySeq()兩個方法,我就舉個很實用的例子。

現實中每每咱們有這樣的一個需求:好比http請求返回體爲json字符串,咱們每每只須要獲取json中的部分字段值。

這個需求,Java的作法是,必須知道全部json中全部字段及類型,並定義對應的JavaBean,將返回的json字符串先轉換爲對應的JavaBean,再經過javaBean獲取想要的字段信息。

那麼Scala使用模式匹配就很輕鬆搞定直接獲取想要的字段值。以下:

先定義一個unapplySeq()實現json的模式匹配(另json字符串插值寫法可參考我之前文章:Scala字符串插值 - StringContext):

 
 
package spray.json

import scala.collection.SortedMap
class JsonInterpolation(sc: StringContext) {
  object json {
    def apply(args: JsValue*): JsValue =
      new JsonParser(ParserInput(sc, args), true).parseJsValue()

    def unapplySeq(input: JsValue): Option[Seq[JsValue]] = {

      val placeHolders = Seq.range(0, sc.parts.length-1).map(x => JsNumber(Integer.MAX_VALUE - x) )

      val pi = ParserInput(sc, placeHolders)
      val pattern = new JsonParser(pi, true).parseJsValue()

      val results = collection.mutable.ArrayBuffer[JsValue]()
      Seq.range(0, sc.parts.length-1).foreach { x => results += null }

      try {
        patternMatch(pattern, input, placeHolders, results)
        Some(results.toSeq)
      }
      catch {
        case ex: Throwable => None
      }

    }

    // TODO report friendly
    private def patternMatch(pattern: JsValue, input: JsValue, placeHolders: Seq[JsValue], results: collection.mutable.ArrayBuffer[JsValue]): Unit = {

      def isPlaceHolder(value: JsNumber) = {
        val num = value.value.toInt
        val index = Integer.MAX_VALUE - num.toInt
        num > 0 && index < placeHolders.size && placeHolders(index).eq(value)
      }

      pattern match {
        case x: JsObject =>
          x.fields.foreach {
            case (key, n @ JsNumber(num)) if isPlaceHolder(n) =>
              val index = Integer.MAX_VALUE - num.toInt
              assert(input.asJsObject.fields contains key)
              results(index) = input.asJsObject.fields(key)

            case (key, value) =>
              assert(input.asJsObject.fields contains key)
              patternMatch(value, input.asJsObject.fields(key), placeHolders, results)
          }
        case x: JsArray =>
          assert(input.isInstanceOf[JsArray])
          assert(input.asInstanceOf[JsArray].elements.size >= x.elements.size)
          x.elements.zipWithIndex.foreach {
            case (x: JsNumber, y: Int) if isPlaceHolder(x) =>
              val index = Integer.MAX_VALUE - x.value.toInt
              results(index) = input.asInstanceOf[JsArray].elements(y)
            case (x: JsValue,y: Int)=>
              patternMatch(x, input.asInstanceOf[JsArray].elements.apply(y), placeHolders, results)
          }
        case x: JsString =>
          assert(x == input)
        case x: JsBoolean =>
          assert(x == input)
        case x: JsNumber =>
          assert(x == input)
        case x@ JsNull =>
          assert(x == input)
      }
    }
  }
}

 

定義好unpplySeq(),那麼下面就看下如何經過模式匹配獲取json字符傳中咱們關係的字段值:

import spray.json._                                             
                                                                
/**                                                             
  * 類功能描述:                                                      
  *                                                             
  * @author WangXueXing create at 19-5-11 下午10:37               
  * @version 1.0.0                                              
  */                                                            
object ObjectMatch {                                            
  def main(args: Array[String]): Unit = {                       
    val json = json"""{"id": 12333,"sku_no":"sku35352523"}"""   
    json match {                                                
      case json"""{"sku_no": $skuNo}""" =>  println(skuNo)      
      case _ => println(json)                                   
    }                                                           
                                                                
  }                                                             
}                                                               

輸出:

"sku35352523"

如上,咱們就能夠選擇性的拿到sku_no字段的值,是否是輕鬆搞定!

相關文章
相關標籤/搜索