初次接触*抽取器(Extractor)这个概念,有点不好理解,可能是本人英语不过关。经过反复推敲,总算弄明白是什么一回事.
以下的讨论基于scala 2.8
还是从一个例子说起。 假设我们想验证一个字符串的格式是否符合邮件地址格式,如果是,提取它的用户部分和域名部分。比如给定字符串jack@163.com
, 经过测试发现符合邮件地址格式,然后提取出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 = "jack@163.com"
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类型. 假如你希望过滤出邮件用户名不含有大写字母, 且是由重复的两部分组成的,比如coco@hotmail.com,你可以这样做:
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("coco@hotmail.com")
userTwiceLower("COco@hotmail.com")
userTwiceLower("ggd543@gmail.com")
当然你完全可以用正则表达式实现,但我相信那样做要复杂得多,而且缺乏灵活性。
假如你需要匹配或分解selector的多个组成部分, 而事先又不确定有多少,你可以使用unapplySeq
方法。unapplySeq
的用法跟unapply差不多,但必须返回Option[Seq[T]]类型。 比如我需要把ggd543@gdut.edu.cn的邮件域名各组成部分提取出来, 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("stu_jack@gdut.edu.cn")
当然使用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进行模式匹配,官方给出的建议是:
- 如果你定义的数据结构或接口仅限于内部使用,而且不会经常变更,推荐使用case class
- 如果接口是给别人用,或面对的是一些遗留类, 推荐使用extractor.
- 如果你拿不准,可以先采用case class,当发现case class 不能适应需求的变化时 ,再改用extractor。使用case class进行模式匹配有一个好处,就是编译器能优化你的代码。假如你的case class是继承自一个封装类(被seal关键字修饰的类),那么编译器还可以对你的match表达式进行检查,并提醒你是否遗漏了某些可能的情况。而extractor比较灵活,能实现你希望的匹配逻辑,但运行效率上比case class要慢。