scala系列(十一)Case Class与模式匹配 有更新!

  |   0 评论   |   1,261 浏览

37套精品Java架构师高并发高性能高可用分布式集群电商缓存性能调优设计项目实战视教程 置顶! 有更新!

(一)模式匹配入门

Java语言中存在switch语句,例如:

package com.boom.scala.sa11;

//下面的代码演示了java中switch语句的使用

public class SwitchDemo {

    public static void main(String[] args) {

        for(int i = 0; i < 100; i++) { 

            switch (i) {

            case 10:System.out.println("10");

                break;

            //在实际编码时,程序员很容易忽略break语句

            //这容易导致意外掉入另外一个分支

            case 50:System.out.println("50");

            case 80:System.out.println("80");

            default:

                break;

            }

        }

    }

}

Scala解决了java语言中存在的这个问题,scala解决这一问题的利器就是模式匹配,上面的java代码可以利用scala语言的模式匹配来避免,代码如下:

object PatternMatching extends App{

  for(i<- 1 to 100){

    i  match {

      case 10 => println(10)

      case 50 => println(50)

      case 80 => println(80)

      case _ => 

    }

  }

}

上述scala代码展示了如何使用scala中的模式匹配,它的实现方式是通过match关键字与 case X=>的方式实现的,其中case _表示除了 case 10,case 50,case 80的其余匹配,类似于java中的default。但scala语言中提供了更为灵活的匹配方式,如:

object PatternMatching2 extends App{

  for(i<- 1 to 100){

    i  match {

      case 10 => println(10)

      case 50 => println(50)

      case 80 => println(80)

      //增加守卫条件

      case _ if(i%4==0)=> println(i+":能被4整除")

      case _ if(i%3==0)=> println(i+":能被3整除")

      case _ =>

    }

  }

}

case语言中还可以加相应的表达式,例如:

object PatternMatching3 extends App{

  var list=new ArrayBuffer[Int]()

  var x=0

  for(i<- 1 to 100){

    i  match {

      //后面可以跟表达式

      case 10 => x=10

      case 50 => println(50)

      case 80 => println(80)

      case _ if(i%4==0)=> list.append(i)

      case _ if(i%3==0)=> println(i+":能被3整除")

      case _ =>

    }

  }

  println(x)

}

 

(二)Case Class简介

Case Class一般被翻译成样例类,它是一种特殊的类,能够被优化以用于模式匹配,下面的代码定义了一个样例类:

//抽象类Person

abstract class Person

//case class Student

case class Student(name:String,age:Int,studentNo:Int) extends Person

//case class Teacher

case class Teacher(name:String,age:Int,teacherNo:Int) extends Person

//case class Nobody

case class Nobody(name:String) extends Person

object CaseClassDemo{

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

    //case class 会自动生成apply方法,从而省去new操作

    val p:Person=Student("john",18,1024)  

    //match case 匹配语法  

    p  match {

      case Student(name,age,studentNo)=>println(name+":"+age+":"+studentNo)

      case Teacher(name,age,teacherNo)=>println(name+":"+age+":"+teacherNo)

      case Nobody(name)=>println(name)

    }

  }

}

当一个类被声名为case class的时候,scala会帮助我们做下面几件事情: 

1.构造器中的参数如果不被声明为var的话,它默认的话是val类型的,但一般不推荐将构造器中的参数声明为var 

2.自动创建伴生对象,同时在里面给我们实现子apply方法,使得我们在使用的时候可以不直接显示地new对象 

3.伴生对象中同样会帮我们实现unapply方法,从而可以将case class应用于模式匹配,关于unapply方法我们在后面的“提取器”那一节会重点讲解 

实现自己的toString、hashCode、copy、equals方法 除此之此,case class与其它普通的scala类没有区别

(三)case class应用实战

1. case class常用方法 

前面我们提到,定义case class便会自动生成对应的toString,hashCode,equals,copy等方法

//toString方法演示

scalaval s=Teacher("john",38,1024)

s: Teacher = Teacher(john,38,1024)

//无参copy方法演示

scalaval s1=s.copy()

s1: Teacher = Teacher(john,38,1024)

//copy方法是深度拷贝

scala> println(s eq s1)false

//equal方法根据对象内容进行比较

scala> println(s equals s1)true

 

scala> println(s == s1)true

//hashcode方法

scala> s1.hashCode

res45: Int = 567742485

//toString方法

scala> s1.toString

res46: String = Teacher(john,38,1024)

//带一个参数的copy方法

scala> s1.copy(name="stephen")

res47: Teacher = Teacher(stephen,38,1024)//带二个参数的copy方法

scala> s1.copy(name="stephen",age=58)

res49: Teacher = Teacher(stephen,58,1024)//带三个参数的copy方法

scala> s1.copy(name="stephen",age=58,teacherNo=2015)

res50: Teacher = Teacher(stephen,58,2015)

 

2. 多个参数的case class

abstract class Person

case class Student( name:String, age:Int, studentNo:Int) extends Person

case class Teacher( name:String, age:Int, teacherNo:Int) extends Person

case class Nobody( name:String) extends Person

//SchoolClass为接受多个Person类型参数的类

case class SchoolClass(classDescription:String,persons:Person*)

//下列代码给出的是其模式匹配应用示例

object CaseClassDemo{

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

     val sc=SchoolClass("dd小星星",Teacher("摇摆少年梦",27,2017),Student("摇摆少年梦",27,2017))

     sc match{

       case SchoolClass(_,_,Student(name,age,studetNo))=>println(name)

       case _ => println("Nobody")

     }

  }

}

 

3. sealed case class

在进行模式匹配的时候,有些时候需要确保所有的可能情况都被列出,此时常常会将case class的超类定义为sealed(密封的) case class,如:

//Person最前面加了个关键字sealed

sealed abstract class Person

case class Student( name:String, age:Int, studentNo:Int) extends Person

case class Teacher( name:String, age:Int, teacherNo:Int) extends Person

case class Nobody( name:String) extends Person

case class SchoolClass(classDescription:String,persons:Person*)

object CaseClassDemo{

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

     val s:Person=Student("john",18,1024)

     //这边仅仅给出了匹配Student的情况,在编译时

     //编译器会提示

     //match may not be exhaustive. It would fail on the following inputs: Nobody(_), Teacher(_, _, _)

     s match{

       case Student(name,age,studentNo)=>println("Student")

     }

  }

}

 

编译器给出的提示可以通过下列语句进行消除

     //下面的语句达到了sealed class的要求

     s match{

       case Student(name,age,studentNo)=>println("Student")

       case Teacher(name,age,studentNo)=>println("Teacher")

       case Nobody(name)=>println("Nobody")

     }

 

4. case class在实用应用中的其它用途 

某个类一旦被定义为case class,则编译器会自动生成该类的伴生对象,伴生对象中包括了apply方法及unapply方法,apply方法使得我们可以不需要new关键字就可以创建对象,而unapply方法,则使得可以方便地应用在模式匹配当中,另外编译器还自动地帮我们实现对应的toString、equals、copy等方法。在实际中,case class除了在模式匹配时能发挥其强大的威力之外,在进行其它应用时,也显示出了其强大的功能,下面给出case class在SparkSQL中的应用,旨在说明case class在实际应用中的重要地位。

 

(四)模式的类型

1. 常量模式

object ConstantPattern{

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

    //注意,下面定义的是一个函数

    //函数的返回值利用的是模式匹配后的结果作为其返回值

    //还需要注意的是函数定义在main方法中

    //也即scala语言可以在一个函数中定义另外一个函数

    def patternShow(x:Any)=x match {

      case 5 => "five"

      case true=>"true"

      case "test"=>"String"

      case null=>"null"

      case Nil=>"empty list"

      case _  =>"Other constant"

    }    

    println(patternShow(5))

  }

}

 

2. 变量模式

object VariablePattern{

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

    def patternShow(x:Any)=x match {

      case 5 => "five"

      //所有不是值为5的都会匹配变量y

      //例如"xxx",则函数的返回结果就是"xxx"

      case y => y

    }   

    println(patternShow("xxx"))

  }

}

3. 构造器模式

//构造器模式必须将类定义为case class

case class Person2(name:String,age:Int)

object ConstructorPattern {

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

      val p=new Person2("摇摆少年梦",27)

      def constructorPattern(p:Person2)=p match {

        case Person2(name,age) => "Person"

        case _ => "Other"

      }

  }

}

 

4. 序列(Sequence)模式 

序列模式指的是像Array、List这样的序列集合进行模式匹配

 

object SequencePattern {

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

      val p=List("spark","hive","SparkSQL")

      def sequencePattern(p:List[String])=p match {

        //只需要匹配第二个元素

        case List(_,second,_*) => second

        case _ => "Other"

      }

      println(sequencePattern(p))

  }

}

 

5. 元组模式

//匹配某个元组内容

object TuplePattern {

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

      val t=("spark","hive","SparkSQL")

      def tuplePattern(t:Any)=t match {

        case (one,_,_) => one

        case _ => "Other"

      }

      println(tuplePattern(t))

  }

}

 

6. 类型模式

//匹配传入参数的类型

object TypePattern {

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

 

      def tuplePattern(t:Any)=t match {

        case t:String=> "String"

        case t:Int => "Integer"

        case t:Double=>"Double"

      }

      println(tuplePattern(5.0))

  }

}

 

上述代码如果不用模式匹配的话,要实现相同的功能,可以通过下列代码实现:

def tuplePattern2(t:Any)={

        if(t.isInstanceOf[String]) "String"

        else if(t.isInstanceOf[Int]) "Int"

        else if(t.isInstanceOf[Double]) "Double"

        else if(t.isInstanceOf[Map[_,_]]) "MAP"

      }

7. 变量绑定模式

object VariableBindingPattern {

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

       var t=List(List(1,2,3),List(2,3,4))      

       def variableBindingPattern(t:Any)= t match {

         //变量绑定,采用变量名(这里是e)

         //与@符号,如果后面的模式匹配成功,则将

         //整体匹配结果作为返回

         case List(_,e@List(_,_,_)) => e

         case _ => Nil

       }

 

       println(variableBindingPattern(t))

  }

}//编译执行后的输出结果为  List(2, 3, 4)

 

(五)for控制结构中的模式匹配

object PatternInForLoop {

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

    val m=Map("china"->"beijing","dwarf japan"->"tokyo","Aerican"->"DC Washington")

    //利用for循环对Map进行模式匹配输出,

    for((nation,capital)<-m)

      println(nation+": " +capital)

  }

}

正则表达式中的模式匹配

object RegexMatch {

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

    val ipRegex="(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)".r

    for(ipRegex(one,two,three,four) <- ipRegex.findAllIn("192.168.1.1"))

    {

      println("IP子段1:"+one)

      println("IP子段2:"+two)

      println("IP子段3:"+three)

      println("IP子段4:"+four)

    }

  }

}

 

(六)Option类型模式匹配

在前面的课程内容中,我们曾经提到过Option类型,Option类型有两个子类,分别是Some和None(单例对象),本小节将从模式匹配的角度对Option类进行重新思考。

下面给出的是Option类在Scala语言中的类层次结构: 

Option类其实是一个sealed class

//Option类的部分源码

sealed abstract class Option[+A] extends Product with Serializable {

  self =>

 

  /** Returns true if the option is $none, false otherwise.

   */

  def isEmpty: Boolean

 

  /** Returns true if the option is an instance of $some, false otherwise.

   */

  def isDefined: Boolean = !isEmpty

 

下面给出的分别是Some及None的源码:

/** Class `Some[A]` represents existing values of type

 *  `A`.

 *

 *  @author  Martin Odersky

 *  @version 1.0, 16/07/2003

 */final case class Some[+A](x: A) extends Option[A] {

  def isEmpty = false

  def get = x

}

 

/** This case object represents non-existent values.

 *

 *  @author  Martin Odersky

 *  @version 1.0, 16/07/2003

 */case object None extends Option[Nothing] {

  def isEmpty = true

  def get = throw new NoSuchElementException("None.get")

}

下面的代码演示了其如何应用到模式匹配中:

object OptionDemo extends App{

  val m=Map("hive"->2,"spark"->3,"Spark MLlib"->4)

 

  def mapPattern(t:String)=m.get(t) match {

    case Some(x) => println(x);x

    case None => println("None");-1

  }

 

  println(mapPattern("Hive"))

}//输出结果为://None//-1

前面我们看到:None是一个case object,它同Some一样都extends Option类,只不过Some是case class,对于case class我们已经很熟悉了,那case object它又是怎么样的呢?假设我们定义了以下类:

//下面的类主要用于模拟Option,Some,None三个类或对象之间的关系

sealed abstract class A

case class B(name:String,age:Int) extends A

case object CaseObject extends A{

 

}

对比上述代码不难看出,case object与case class所不同的是,case object对应反编译后的CaseObject$.class中不存在apply、unapply方法,这是因为None不需要创建对象及进行内容提取,从这个角度讲,它被定义为case object是十分合理的。

 

(七)偏函数与函数加里化currying)

http://www.vaikan.com/currying-partial-application/

函数加里化(Currying)明显解决的是一个完全不同的问题:如果我们有几个单参数函数,并且这是一种支持一等函数(first-class)的语言,如何去实现一个多参数函数?函数加里化是一种实现多参数函数的方法。

函数加里化和偏函数应用的总结

1. 偏函数应用是找一个函数,固定其中的几个参数值,从而得到一个新的函数。

2. 函数加里化是一种使用匿名单参数函数来实现多参数函数的方法。

3. 函数加里化能够让你轻松的实现某些偏函数应用。

4. 有些语言(例如 Haskell, OCaml)所有的多参函数都是在内部通过函数加里化实现的

(八)case与偏函数

1. 从使用case语句构造匿名函数谈起

Scala里,我们可以使用case语句来创建一个匿名函数(函数字面量),这有别于一般的匿名函数创建方法。来看个例子:

scala> List(1,2,3) map {case i:Int=>i+1}

res1: List[Int] = List(234)

这很有趣,case i:Int=>i+1构建的匿名函数等同于(i:Int)=>i+1,也就是下面这个样子:

scala> List(1,2,3) map {(i:Int)=>i+1}res2: List[Int] = List(234)

scala In Programming》一书对独立的case语句作为匿名函数(函数字面量)有权威的解释:

Essentially, a case sequence is a function literal, only more general. Instead of having a single entry point and list of parameters, a case sequence has multiple entry points, each with their own list of parameters. Each case is an entry point to the function, and the parameters are specified with the pattern. 

一个case语句就是一个独立的匿名函数,如果有一组case语句的话,从效果上看,构建出的这个匿名函数会有多种不同的参数列表,每一个case对应一种参数列表,参数是case后面的变量声明,其值是通过模式匹配赋予的。

2. 使用case语句构造匿名函数的“额外”好处

使用case语句构造匿名函数是有“额外”好处的,这个“好处”在下面这个例子中得到了充分的体现:

List(135"seven") map { case i: Int => i + 1 } // won't work// scala.MatchError: seven (of class java.lang.String)

List(135"seven") collect { case i: Int => i + 1 }// verify

assert(List(246) == (List(135"seven") collect { case i: Int => i + 1 }))

  def collect[B, That](pf: PartialFunction[A, B])(implicit bf: CanBuildFrom[Repr, B, That]): That = {

    val b = bf(repr)

    for (x <- thisif (pf.isDefinedAt(x)) b += pf(x)

    b.result

  }

在这个例子中:传递给map的case语句构建的是一个普通的匿名函数,在把这个函数适用于”seven”元素时发生了类型匹配错误。而对于collect,它声明接受的是一个偏函数:PartialFunction,传递的case语句能良好的工作说明这个case语句被编译器自动编译成了一个PartialFunction!这就是case语句“额外”的好处:case语句(组合)除了可以被编译为匿名函数(类型是FunctionX,在Scala里,所有的函数字面量都是一个对象,这个对象的类型是FunctionX),还可以非常方便的编译为一个偏函数PartialFunction!(注意:PartialFunction同时是Function1的子类)编译器会根据调用处的函数类型声明自动帮我们判定如何编译这个case语句(组合)

上面我们直接抛出了偏函数的概念,这会让人头晕,我们可以只从collect这个示例的效果上去理解偏函数:它只对会作用于指定类型的参数或指定范围值的参数实施计算,超出它的界定范围之外的参数类型和值它会忽略(未必会忽略,这取决于你打算怎样处理)就像上面例子中一样,case i: Int => i + 1只声明了对Int参数的处理,在遇到”seven”元素时,不在偏函数的适用范围内,所以这个元素被忽略了。

3. 正式认识偏函数Partial Function

如同在一开始的例子中那样,我们手动实现了一个与case i:Int=>i+1等价的那个匿名函数(i:Int)=>i+1,那么在上面的collect方法中使用到的case i: Int => i + 1它的等价函数是什么呢?显然,不可能是(i:Int)=>i+1了,因为我们已经解释了,collect接受的参数类型是PartialFunction[Any,Int],而不是(Int)=>Int 那个case语句对应的偏函数具体是什么样的呢?来看:

scala> val inc = new PartialFunction[Any, Int] {

     | def apply(any: Any) = any.asInstanceOf[Int]+1

     | def isDefinedAt(any: Any) = if (any.isInstanceOf[Int]) true else false

     | }

inc: PartialFunction[Any,Int] = <function1>

scala> List(1, 3, 5, "seven") collect inc

res4: List[Int] = List(2, 4, 6)

PartialFunction特质规定了两个要实现的方法:apply和isDefinedAt,isDefinedAt用来告知调用方这个偏函数接受参数的范围,可以是类型也可以是值,在我们这个例子中我们要求这个inc函数只处理Int型的数据。apply方法用来描述对已接受的值如何处理,在我们这个例子中,我们只是简单的把值+1,注意,非Int型的值已被isDefinedAt方法过滤掉了,所以不用担心类型转换的问题。

上面这个例子写起来真得非常笨拙,和前面的case语句方式比起来真是差太多了。这个例子从反面展示了:通过case语句组合去是实现一个偏函数是多么简洁。实际上case语句组合与偏函数的用意是高度贴合的,所以使用case语句组合是最简单明智的选择,同样是上面的inc函数,换成case去写如下:

scaladef inc: PartialFunction[Any, Int] =

     | { case i: Int => i + 1 }

inc: PartialFunction[Any,Int]

 

scala> List(135"seven") collect inc

res5: List[Int] = List(246)

当然,如果偏函数的逻辑非常复杂,可能通过定义一个专门的类并继承PartialFunction是更好选择。

4. Case语句是如何被编译成偏函数的

关于这个问题在《Programming In Scala》中有较为详细的解释。对于这样一个使用case写在的偏函数:

val second: PartialFunction[List[Int],Int] = {

    case x :: y :: _ => y

}

In fact, such an expression gets ranslated by the Scala compiler to a partial function by translating the patterns twice—once for the implementation of the real function, and once to test whether the function is defined or not. For instance, the function literal{ case x :: y :: _ => y }above gets translated to the following partialfunction value:

new PartialFunction[List[Int], Int] {

    def apply(xs: List[Int]) = xs match {

        case x :: y :: _ => y

    }

    def isDefinedAt(xs: List[Int]) = xs match {

        case x :: y :: _ => true

        case _ => false

    }

}

5. 为什么偏函数需要抽象成一个专门的Trait

首先,在Scala里,一切皆对象,函数字面量(匿名函数)也不例外!这也是为什么我们可以把函数字面量赋给一个变量的原因, 是对象就有对应的类型,那么一个函数字面量的真实类型是什么呢?看下面这个例子:

scalavar inc = (x: Int) => x + 1inc: Int => Int = <function1>

 

scala> inc.isInstanceOf[Function1[Int,Int]]res0: Boolean = true

Scala的scala包里,有一系列Function trait,它们实际上就是函数字面量作为“对象”存在时对应的类型。Function类型有多个版本,Function0表示无参数函数,Function1表示只有一个参数的函数,以此类推。至此我们解释的是一个普遍性问题:是函数就是对象,是对象就有类型。那么,接下来我们看一下偏函数又应该是什么样的一种“类型”?

从语义上讲,偏函数区别于普通函数的唯一特征就是:偏函数会自主地告诉调用方它的处理参数的范围,范围既可是值也可以是类型。针对这样的场景,我们需要给函数安插一种明确的“标识”,告诉编译器:这个函数具有这种特征。所以特质PartialFunction就被创建出来用于“标记”这类函数的,这个特质最主要的方法就是isDefinedAt!同时你也记得PartialFunction还是Function1的子类,所以它也要有apply方法,这是非常自然的,偏函数本身首先是一个函数嘛。

从另一个角度思考,偏函数的逻辑是可以通过普通函数去实现的,只是偏函数是更为优雅的一种方式,同时偏函数特质PartialFunction的存在对调用方和实现方都是一种语义更加丰富的约定,比如collect方法声明使用一个偏函数就暗含着它不太可能对每一个元素进行操作,它的返回结果仅仅是针对偏函数“感兴趣”的元素计算出来的

6. 为什么偏函数只能有一个参数?

为什么只有针对单一参数的偏函数,而不是像Function特质那样,拥有多个版本的PartialFunction呢?在刚刚接触偏函数时,这也让我感到费解,但看透了偏函数的实质之后就会觉得很合理了。我们说所谓的偏函数本质上是由多个case语句组成的针对每一种可能的参数分别进行处理的一种“结构较为特殊”的函数,那特殊在什么地方呢?对,就是case语句,前面我们提到,case语句声明的变量就是偏函数的参数,既然case语句只能声明一个变量,那么偏函数受限于此,也只能有一个参数!说到底,类型PartialFunction无非是为由一组case语句描述的函数字面量提供一个类型描述而已,case语句只接受一个参数,则偏函数的类型声明自然就只有一个参数。

但是,上这并不会对编程造成什么阻碍,如果你想给一个偏函数传递多个参数,完全可以把这些参数封装成一个Tuple传递过去!

 


评论

发表评论

validate