1. 程式人生 > >第二十五節 提取器(Extractor)

第二十五節 提取器(Extractor)

本節主要內容

  1. apply與unapply方法
  2. 零變數或變數的模式匹配
  3. 提取器與序列模式
  4. scala中的佔位符使用總結

1. apply與unapply方法

apply方法我們已經非常熟悉了,它幫助我們無需new操作就可以建立物件,而unapply方法則用於析構出物件,在模式匹配中特別提到,如果一個類要能夠應用於模式匹配當中,必須將類宣告為case class,因為一旦被定義為case class,Scala會自動幫我們生成相應的方法,這些方法中就包括apply方法及unapply方法。本節將從提取器(也稱析構器)的角度對unapply方法進行介紹。先看下面的這個例子(來源於programmin in scala)

object EMail{
  //apply方法用於無new構造物件
  def apply(user: String, domain: String) = user + "@" + domain
  //unapply方法用於在模式匹配中充當extractor
  def unapply(str: String): Option[(String, String)] = {
    val parts = str split "@"
    if (parts.length == 2) Some(parts(0), parts(1)) else None
  }
}
object ApplyAndUnapply
{
val email=EMail("zhouzhihubeyond","sina.com") //下面的匹配會導致呼叫EMail.unapply(email) case EMail(user,domain) => println("user="+user+" domain="+domain) } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

在上述的程式碼中,我們將unapply方法定義為: 
def unapply(str: String): Option[(String, String)] = { 
val parts = str split "@" 
if (parts.length == 2) Some(parts(0), parts(1)) else None 
}

這是有道理的,原因在於可能會有不合法的email用於模式匹配,例如:

object ApplyAndUnapply extends App{
  def patternMatching(x:String)=x match {
    //下面的匹配會導致呼叫EMail.unapply(email)
    case EMail(user,domain) => println("user="+user+" domain="+domain)
    //匹配非法郵箱
    case _ => println("non illegal email")
  }
  val email=EMail("zhouzhihubeyond","sina.com")
  patternMatching(email) 
  patternMatching("搖擺少年夢") 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

從構造與析構的角度來看,apply方法也被稱為injection(注入),unapply方法也被稱為提取器,這兩個方法就像孿生兄弟一樣,經常在類或物件中被定義。以前我們在用類進行模式匹配的時候都必須要將類宣告為case class,今天我們將不通過case class,而是定義一個普通的類實現自己的apply和unapply方法來實現模式匹配,程式碼如下:

//定義一個普通類
class Person(val firstName:String,val secondName:String)

//在伴生物件中定義apply方法和unapply方法
object Person{
  def apply(firstName: String, secondName: String) = new Person(firstName,secondName)

 def unapply(person: Person):Option[(String,String)]={
    if(person!=null) Some(person.firstName,person.secondName)
    else None
  }
}


val p=Person("搖擺少年夢","周")
  p match {
    //析構出firstName,secondeName
    case Person(firstName,secondName) => println("firstName="+firstName+" secondName="+secondName)
    case _ => println("null object")
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

2. 零變數或單變數繫結的模式匹配

上一節講的模式模式匹配繫結的是兩個變數,它可以擴充套件到任意變數維度,這一節中我們對零變數和單個變數繫結的特殊情況進行介紹,我們來看下面的這個例子,該例子來源於 programmin in scala

//Twice用於匹配重複出現的字串,它繫結的是一個變數
//即返回的型別是Option[String]
object Twice {
  def apply(s: String): String = s + s
  def unapply(s: String): Option[String] = {
    val length = s.length / 2
    val half = s.substring(0, length)
    if (half == s.substring(length)) Some(half) else None
  }
}
//未繫結任何變數,僅僅返回Boolean型別
object UpperCase {
  def unapply(s: String): Boolean = s.toUpperCase == s
}

object NonAndOneVariablePattern extends App{
  def userTwiceUpper(s: String) = s match {
    //下面的程式碼相當於執行了下面這條語句
    //UpperCase.unapply(Twich.unapply(EMail.unapply(s)))
    case EMail(Twice(x @ UpperCase()), domain) =>
      "match: " + x + " in domain " + domain
    case _ =>
      "no match"
  }
  val email=EMail("搖擺少年夢搖擺少年夢","sina.com")
  println(userTwiceUpper(email))
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

程式碼中的EMail(Twice(x @ UpperCase()),其執行順序是先呼叫EMail的unapply方法,然後再呼叫Twice中的unapply方法,最後呼叫UpperCase的unapply方法,如果返回true,則將Twice 中返回的字串賦值給x。

3. 提取器與序列模式

List伴生物件具有下列定義形式:

object List {
def apply[T](elems: T*) = elems.toList
def unapplySeq[T](x: List[T]): Option[Seq[T]] = Some(x)
...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

從上面的程式碼來看,與一般的提取器不同的是,序列模式採用unapplySeq代替unapply方法,並且返回的型別是Option[Seq[T]] ,在講模式匹配的時候我們提到過,序列模式中的匹配經常會使用佔位符_或_*的方式匹配序列中的其它元素,這種方式為序列模式所獨有,例如:

object ExtractorSequence extends App{
  val list=List(List(1,2,3),List(2,3,4))
  list match {
    //_*表示匹配列表中的其它元素
    case List(List(one,two,three),_*) => 
      println("one="+one+" two="+two+" three="+three)
    case _ => println("Other")
  }
  list match {
    //_表示匹配列表中的第一個元素
    //_*表示匹配List中的其它多個元素
    //這裡採用的變數繫結的方式
    case List(_,x@List(_*),_*) => println(x)
    case _ => println("other list")
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

4. scala中的佔位符使用總結

scala作為一種函數語言程式設計語言,有很多地方會讓初學者覺得困惑,其中佔位符_的使用理解有一定的難度,本節將對其使用進行總結,本小節內容來源http://my.oschina.net/leejun2005/blog/405305,感謝作者的無私奉獻。

1、存在性型別:Existential types
def foo(l: List[Option[_]]) = ...

2、高階型別引數:Higher kinded type parameters
case class A[K[_],T](a: K[T])

3、臨時變數:Ignored variables
val _ = 5

4、臨時引數:Ignored parameters
List(1, 2, 3) foreach { _ => println("Hi") }

5、通配模式:Wildcard patterns
Some(5) match { case Some(_) => println("Yes") }
match {
     case List(1,_,_) => " a list with three element and the first element is 1"
     case List(_*)  => " a list with zero or more elements "
     case Map[_,_] => " matches a map with any key type and any value type "
     case _ =>
 }
val (a, _) = (1, 2)
for (_ <- 1 to 10)

6、通配匯入:Wildcard imports
import java.util._

7、隱藏匯入:Hiding imports
// Imports all the members of the object Fun but renames Foo to Bar
import com.test.Fun.{ Foo => Bar , _ }

// Imports all the members except Foo. To exclude a member rename it to _
import com.test.Fun.{ Foo => _ , _ }

8、連線字母和標點符號:Joining letters to punctuation
def bang_!(x: Int) = 5

9、佔位符語法:Placeholder syntax
List(1, 2, 3) map (_ + 2)
_ + _   
( (_: Int) + (_: Int) )(2,3)

val nums = List(1,2,3,4,5,6,7,8,9,10)

nums map (_ + 2)
nums sortWith(_>_)
nums filter (_ % 2 == 0)
nums reduceLeft(_+_)
nums reduce (_ + _)
nums reduceLeft(_ max _)
nums.exists(_ > 5)
nums.takeWhile(_ < 8)

10、偏應用函式:Partially applied functions
def fun = {
    // Some code
}
val funLike = fun _

List(1, 2, 3) foreach println _

1 to 5 map (10 * _)

//List("foo", "bar", "baz").map(_.toUpperCase())
List("foo", "bar", "baz").map(n => n.toUpperCase())

11、初始化預設值:default value
var i: Int = _

12、作為引數名:
//訪問map
var m3 = Map((1,100), (2,200))
for(e<-m3) println(e._1 + ": " + e._2)
m3 filter (e=>e._1>1)
m3 filterKeys (_>1)
m3.map(e=>(e._1*10, e._2))
m3 map (e=>e._2)

//訪問元組:tuple getters
(1,2)._2

13、引數序列:parameters Sequence 
_*作為一個整體,告訴編譯器你希望將某個引數當作引數序列處理。例如val s = sum(1 to 5:_*)就是將1 to 5當作引數序列處理。
//Range轉換為List
List(1 to 5:_*)

//Range轉換為Vector
Vector(1 to 5: _*)

//可變引數中
def capitalizeAll(args: String*) = {
  args.map { arg =>
    arg.capitalize
  }
}

val arr = Array("what's", "up", "doc?")
capitalizeAll(arr: _*)