初次接触这个概念(Extractor),有点不好理解,可能是本人英语不过关。经过反复推敲,总算弄明白是什么一回事. 还是从一个例子说起。
假设我们想验证一个字符串的格式是否符合邮件地址格式,如果是,提取它的用户部分和域名部分。比如给定字符串"[email protected]" , 经过测试发现符合邮件地址格式,然后提取出"jack"和"163.com"。一般的做法是通过正则表达式进行匹配并提取相应的匹配组.
在Scala中,可以通过模式匹配实现。具体做法是,给一个对象(不妨叫obj)定义一个unapply方法,且该方法必须返回Option[T]类型; 在使用该对象与给定的参数(不妨叫selector)进行模式匹配时,会调用该对象的unapply方法; 调用unapply方法时会传入一个参数,该参数就是待匹配的参数(selector);匹配逻辑定义在unapply方法中,假如匹配成功,unapply方法返回Some[T]类型的对象, 否则返回None。Some()所包含的内容没有限制,但一般让它带回匹配的内容。
比如
object Email{
def unapply(str: String): Option[(String, String)] = {
val parts = str split "@"
if (parts.length == 2) Some(parts(0), parts(1)) else None
}
}
val str = "[email protected]"
str match{
case Email(username, address) => println("username: "+username+" address: "+addres);
case _ => println("this is not an email address ");
}
实际上str 与 Email 作模式匹配时 会被翻译成
Email.unapply(str) match{ case Some(username, address) => ... case None => ... }
所谓的extractor就是指含有unapply方法(或unapplySeq方法)的对象.
如果你只想检验该字符串是否为一个合法的邮件地址, 可以让unapply返回Boolean类型.
假如你希望过滤出邮件用户名不含有大写字母, 且是由重复的两部分组成的,比如[email protected],你可以这样做:
object LowerCase{ def unapply(name: String) = name.toLowerCase == name } object Twice{ def unapply(s: String) : Option[String] = { val len = s.length /2 val half = s.substring(0, len); if (half == s.substring(len)) Some(half) else None } } def userTwiceLower(s: String) = s match{ case Email(Twice(x @ LowerCase()), domain) => "match: "+ x + " in domain " + domain case _ => "no mach" } userTwiceLower("[email protected]") userTwiceLower("[email protected]") userTwiceLower("[email protected]")
当然你完全可以用正则表达式实现,但我相信那样做要复杂得多,而且缺乏灵活性。
假如你需要匹配或分解selector的多个组成部分, 而事先又不确定有多少,你可以使用unapplySeq方法。unapplySeq的用法跟unapply差不多,但必须返回Option[Seq[T]]类型。 比如我需要把[email protected]的邮件域名各组成部分提取出来, cn, edu, gdut。 下面的例子匹配邮件用户名前缀为stu_ ,域名为cn的邮件地址:
object Email { ... } object Domain{ def unapplySeq(whole: String) : Option[Seq[String]] ={ Some(whole.split("\\.").reverse); } } object StuPrefix{ def unapply(name: String) = name.startsWith("stu_") } def isStuCnMail(str: String) = str match{ case Email(StuPrefix(), Domain("cn", _*)) => true case _ => false } isStuCnMail("[email protected]")
当然使用unapply也可以,只是写法上没有那么直观.
其实Scala的Array, List等集合类实现了unapplySeq方法,使得我们可以这么写:
val Array(a,b,c,d) = Array(1,2,3,4) val List(head, tail @ _ *) = List(1,2,3,4)
虽然看起来有点像Constructor Pattern.
如果你对extractor的理解仅仅停留在"能实现用户自定义的模式匹配”的技术层面上,那就太肤浅了。 我认为Scala提供Extractor这种语法糖的目的在于,将数据模型和视图逻辑分离,或者说它充当了类似于适配器那样的角色, 而且是一种比较函数式的做法。
至于在实际编程中应该采用case class还是extractor进行模式匹配,官方给出的建议是:
1. 如果你定义的数据结构或接口仅限于内部使用,而且不会经常变更,推荐使用case class
2. 如果接口是给别人用,或面对的是一些遗留类, 推荐使用extractor.
3. 如果你拿不准,可以先采用case class,当发现case class 不能适应需求的变化时 ,再改用extractor
使用case class进行模式匹配有一个好处,就是编译器能优化你的代码。假如你的case class是继承自一个封装类(被seal关键字修饰的类),那么编译器还可以对你的match表达式进行检查,并提醒你是否遗漏了某些可能的情况。而extractor比较灵活,能实现你希望的匹配逻辑,但运行效率上比case class要慢。