Scala之模式匹配pattern-matching与偏函数PartialFunctions

模式匹配(pattern matching)

pattern matching 可以说是 scala 中十分强大的一个语言特性,当然这不是 scala 独有的,但这不妨碍它成为 scala 的语言的一大利器。
要理解模式匹配(pattern-matching),先把这两个单词拆开,先理解什么是模式(pattern),这里所的模式并不是设计模式里的模式。
而是数据结构上的,这个模式用于描述一个结构的组成。
我们很容易联想到“正则表达”里的模式,不错,这个pattern和正则里的pattern相似,不过适用范围更广,可以针对各种类型的数据结构,不像正则表达只是针对字符串。
狭义的看,模式可以当作对某个类型,其内部数据在结构上抽象出来的表达式。
模式匹配(pattern-matching)则是匹配变量是否符合这种pattern。比如List(“A”,”B”)和List(“A”,”X”,”Y”) 就符合上面的pattern,而List(“X”)则不符合。

有6种实现模式匹配的方法

  1. 面向对象的分解 (decomposition)
  2. 访问器模式 (visitor)
  3. 类型测试/类型造型 (type-test/type-cast)
  4. typecase
  5. 样例类 (case class)
  6. 抽取器 (extractor)

最终scala选择了采用 样例类(case class) 和 抽取器(extractor) 来实现模式匹配。

1)样例类(case class)
样例类常用于描述不可变的值对象(Value Object);
本质上case class是个语法糖,对你的类构造参数增加了getter访问,还有自动实现的toString, hashCode, equals 等方法;

样例类构造参数默认声明为“val”,自动实现类构造参数的getter;
样例类构造参数声明为“var”时,自动实现类构造参数的setter和getter方法

定义一个样例类用:case class 类名

最重要的是自动帮你实现了一个伴生对象,这个伴生对象里定义了apply 方法和 unapply 方法。
apply方法是用于在构造对象时,减少new关键字;
而unapply方法则是为模式匹配所服务,接收一个示例对象,返回最初创建它所用的参数,也就是从对象中提取相应的值。
这两个方法可以看做两个相反的行为,apply是构造(工厂模式),unapply是分解(解构模式)

举例:

//定义样例类
case class Student(name:Stirng,age:Int)
//创建样例类的实例,无需new关键字
val stu = Student("abc",12)
//访问对象属性
println(stu.name)

//结果
abc

注意:
case class在暴露了它的构造方式,所以要注意应用场景:当我们想要把某个类型暴露给客户,但又想要隐藏其数据表征时不适宜。

2) 抽取器(extrator)
抽取器是指定义了unapply方法的object。在进行模式匹配的时候会调用该方法。
unapply方法接受一个数据类型,返回另一数据类型,表示可以把入参的数据解构为返回的数据。
举例:

class A
class B(val a:A)
object TT {
def unapply(b:B) = Some(new A)
}

这样定义了抽取器TT后,
看看模式匹配:

val b = new B(new A);
  b match{ 
case TT(a) => println(a)
}

直观上以为 要拿b和TT类型匹配,实际被翻译为
TT.unapply(b) match{
case Some(…) => …
}
它与上面的case class相比,相当于自己手动实现unapply,这也带来了灵活性。

举例:
比如我们想要暴露的类型为A

//定义为抽象类型
trait A
//然后再实现一个具体的子类,有2个构造参数
class B (val p1:String, val p2:String) extends A
//定义一个抽取器
object MM{
//抽取器中apply方法是可选的,这里是为了方便构造A的实例
def apply(p1:String, p2:String) : A = new B(p1,p2);
//把A分解为(String,String)
def unapply(a:A) : Option[(String, String)] = {    
   if (a.isInstanceOf[B]) {
   val b = a.asInstanceOf[B]
   return Some(b.p1, b.p2)
   }
  None
 }
}

这样客户只需要通过 MM(x,y) 来构造和模式匹配了。客户只需要和MM这个工厂/解构角色打交道,A的实现怎么改变都不受影响。

总结
模式匹配其实本质上是提供一个方便的解构 (Destructuring) 数据结构的方式,以 scala 为例, pattern matching 其实用到了 scala 中提取器的功能, 提取器其实就是类中的 unapply () 方法。

trait User {
  def name: String
}
class FreeUser(val name: String) extends User
   object FreeUser {
   //提取器
  def unapply(user: FreeUser): Option[String] = Some(user.name)
}
  val user: User = new FreeUser("Daniel")
  user match {
    case FreeUser(name) => println("it match here" + name)
    case _ => println("not me")
  }

明白了模式匹配的本质你就会知道,其实 if else 只是 pattern matching 中的一个典型的用法,但并非它的全部。

同时, pattern matching 允许你解耦两个并不真正属于彼此的东西,也使得你的代码更易于测试。
比如上面的 match 部分的代码我们可以写成下面这样:

val user: User = new FreeUser("Daniel")
   //将返回结果存在一个常量中
  val message = user match {
    case FreeUser(name) => "it match here" + name
    case _ => "not me"
    }
  //可以随意使用该常量,实现解耦
  println(message)

这样会赋予代码更多的灵活性,同时也更加方便做进一步操作。
而以可读性的角度来说,使用一大堆的 if else 代码无疑是比较难看的,而如果使用 pattern matching 的话,代码会简洁清晰很多,而简洁的代码则会更容易阅读。

模式匹配语法:
match 对应 Java 里的 switch,但是写在选择器表达式之后。即: 选择器 match {备选项}。
每个都开始于关键字 case。每个备选项都包含了一个模式及一到多个表达式。箭头符号 => 隔开了模式和表达式。

变量 : match {
  case1 => 代码1
  case2 => 代码2
  case _   => 代码2
}

举例:基本模式匹配

def matchTest(x: Any): Any = x match {
      case 1 => "one"
      case "two" => 2
      case y: Int => "scala.Int"
      case _ => "many"
   }

解析:
实例中第一个 case 对应整型数值 1,
第二个 case 对应字符串值 two,
第三个 case 对应类型模式,用于判断传入的值是否为整型,相比使用isInstanceOf来判断类型,使用模式匹配更好。
第四个 case 表示默认的全匹配备选项,即没有找到其他匹配时的匹配项,类似 switch 中的 default。

模式守卫(在模式后面加上if判断)

def test(x:Int):String= x match{
  case i if i==1 =>"one"
  case i if i==2 =>"two"
  case _=> "other"

//调用
test(2)     //two
test(1)     //one
test(3)     //other

类型匹配

def test(x:Ant):String=x match{
case x:Int => "Int"
case x:String => "String"
case x:Any => "Any"

//调用
test(3)      //Int
test(3.4)    //Any
test("abc")  //String

使用样例类的模式匹配

使用了case关键字的类定义就是就是样例类(case classes),样例类是种特殊的类,经过优化以用于模式匹配。

举例:

object Test {
case class Person(name:String,age:Int)
   def main(args: Array[String]) {
    val alice = new Person("Alice", 25)
    val bob = new Person("Bob", 32)
    val charlie = new Person("Charlie", 32)
  
   for (person <- List(alice, bob, charlie)) {
        person match {
            case Person("Alice", 25) => println("Hi Alice!")
            case Person("Bob", 32) => println("Hi Bob!")
            case Person(name, age) =>
                 println("Age: " + age + ", name: " + name + "?")
           }
      }
   }
   // 样例类
   case class Person(name: String, age: Int)
}

执行结果:
  Hi Alice!
  Hi Bob!
  Age: 32, name: Charlie?

偏函数PartialFunctions

通俗来说,偏函数特性:就是偏函数会自主地告诉调用方法它的处理参数的范围,范围既可是值也可以是类型。针对这样的场景,我们需要给函数安插一种明确的“标识”,告诉编译器:这个函数具有这种特征。所以特质PartialFunction就被创建出来用于“标记”这类函数的,这个特质最主要的方法就是isDefinedAt!同时你也记得PartialFunction还是Function的子类,所以它也要有apply方法。

也因此PartialFunction特质规定了两个要实现的方法:apply和isDefinedAt

isDefinedAt方法用来告知调用方这个偏函数接受参数的范围,可以是类型也可以是值,在我们这个例子中我们要求这个inc函数只处理Int型的数据。

apply方法用来描述对已接受的值如何处理

语法:
定义方法: 方法名:PartitalFunction[传过来的参数类型,返回的参数类型]

def sayHello:PartialFunction[String,String] = {
    case "函数"  => "偏函"
    case "模式" => "模式匹配"
    }
    println(sayHello("函数"))

collect方法


def inc: PartialFunction[Any, Int] ={
    case i: Int => i + 1
  }
  //结果
inc: PartialFunction[Any,Int]

List(1, 3, 5, "seven") collect inc
//结果
List[Int] = List(2, 4, 6)   

对给定的输入参数类型,函数可接受该类型的任何值。换句话说,一个(Int) => String 的函数可以接收任意Int值,并返回一个字符串。

对给定的输入参数类型,偏函数只能接受该类型的某些特定的值。一个定义为(Int) => String 的偏函数可能不能接受所有Int值为输入。

isDefinedAt 是PartialFunction的一个方法,用来确定PartialFunction是否能接受一个给定的参数。
举例:

val one: PartialFunction[Int, String] = { 
   case 1 => "one" 
}
one.isDefinedAt(1)       // true 
one.isDefinedAt(2)       // false

可以调用一个偏函数
one(1) 			// one 

PartialFunctions可以使用orElse组成新的函数,得到的PartialFunction反映了是否对给定参数进行了定义。
举例:

val two: PartialFunction[Int, String] = {  
   case 2 => "two" 
} 
val three: PartialFunction[Int, String] = { 
   case 3 => "three" 
} 
val wildcard: PartialFunction[Int, String] = { 
   case _ => "something else" 
} 
val partial = one orElse two orElse three orElse wildcard 

调用偏函数
partial(5) // something else 
partial(3) // three  
partial(2) // two 
partial(1) // one  
partial(0) // something else 

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

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

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

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

说的简单点:
偏函数是{}内没有match的一组case语句,
但是不同的是,它是被包含在花括号里面的没有macth这个单词的一组case语句;
只处理自己给的条件的其中那一部分,想要输出哪部分就输出那部分,剩余的部分可以一起输出,也可以再给条件进行筛选。
模式匹配可以通过偏函数来实现。

猜你喜欢

转载自blog.csdn.net/zp17834994071/article/details/107317328