返回介绍

数学基础

统计学习

深度学习

工具

Scala

五、提取器

发布于 2023-07-17 23:38:22 字数 17572 浏览 0 评论 0 收藏 0

  1. 目前为止,构造方法模式都和样例类有关。如 Some(x) 是一个合法的模式,因为 Some 是一个样例类。有时候我们需要提取模式,但是不希望创建相关的样例类,此时需要用到提取器。提取器是模式匹配的进一步泛化。

  2. Scala 中,提取器是拥有名字叫 unapply 成员方法的对象。这个 unapply 方法的目的是跟某个值做匹配,并将其拆解开。

    通常提取器对象还会定义一个跟 unapply 相对应的 apply 方法用于构建值,不过这不是必须的。

    如下所示为一个用于处理 email 地址的提取器对象:

    
    
    xxxxxxxxxx
    object Email{ def apply(user: String, domain: String) = user + "@" + domain // apply 方法不是必须的 def unapply(str: String) :Option[(String,String)] = { val parts = str split "@" if(parts.length == 2) Some(parts(0), partes(1)) else None } }

    这个对象同时定义了 apply 方法和 unapply 方法。

    • apply 方法使得我们可以像调用函数一样调用 Email 对象:Email("huaxz","163.com)

      如果想要更明显的表明意图,还可以让 Email 继承自 Scala 的函数类型:

      
      
      xxxxxxxxxx
      object Email extends ((String,String) => String){ ... }

      这个对象声明当中的 (String,String) => String,其含义跟 Function2[String, String, String] 一样。

    • unapply 方法就是将 Email 变成提取器的核心方法。从某种意义上讲,它是 apply 方法的逆运算。但是 unapply 还需要处理输入的字符串不是email 的情况,这也是为什么 unapply 方法的返回类型为 Option 类型。

  3. 每当模式匹配遇到引用提取器对象的模式时,它都会调用提取器的 unapply 方法。例如:

    
    
    xxxxxxxxxx
    str match{ case Email(user, domain) => ... case ... }

    这将会引发如下调用Email.unapply(str) 。调用结果要么返回 None,要么返回 Some(u,d)

    • 如果返回 Some(u,d),则模式能够匹配,则变量 user 会被绑到返回值 u,变量domain 会绑到返回值 d
    • 如果返回 None,则模式未能匹配。系统会继续尝试匹配下一个模式。如果没有下一个模式,则系统返回 MatchError 异常。

    另外,这里的 str 的类型可以是 Email.unapply 的参数类型,但也可以不是。我们可以用提取器来匹配更笼统的类型表达式:

    
    
    xxxxxxxxxx
    val x: Any = .... x match{ case Email(user, domain) => ... case ... }

    此时:

    • 系统首先会检查给定的值 x 是否满足 Email.unapply 参数类型的要求。如果满足要求,则x 就会被转成StringEmail.unapply 的参数类型),然后继续上述讨论的模式匹配流程。
    • 如果类型不满足要求,则模式未能匹配,系统继续尝试匹配下一个模式。
  4. 在对象 Email 中,apply 方法称作注入 injection,因为它接收某些入参,并交出给定的集合(在我们这里表示能够代表email 地址的字符串集合);unapply 方法称作提取extraction,因为它接收上述集合的一个元素,并将它的某些组成部分提取出来。

    injectionextraction 通常成对地出现在对象中,因为这样一来我们就可以同时表示构造方法和模式。但是我们也可以在不定义injection 的情况下单独定义 extraction

    • 含有 extraction 的对象被称作提取器extractor,无论它是否有 apply 方法。

    • 如果同时包含了 injectionextraction 方法,则它们是一对dual 对偶方法。虽然这不是 scala 语法必须的,但是我们建议这么做。

      
      
      xxxxxxxxxx
      Email.unapply(Email.apply(user, domain)) // 应该返回 Some(user,domain)

5.1 提取 0 个或 1个变量的模式

  1. 前面的 unapply 方法在成功的时候返回一对元素的值,这很容易推广到多个变量的模式。如果需要绑定 N 个变量,则 unapply 方法可以返回一个以 Some 包起来的 N 个元素的元组。

    • 但是,当模式只绑定一个变量时,处理逻辑是不同的。Scala 并没有单个元素的元组,因此unapply 方法只是简单地将元素本身放到 Some 里。

      如以下提取器提取连续相同的子串:

      
      
      xxxxxxxxxx
      object Twice{ def apply(s: String): String = s + s def unapply(s: String): Option[String] = { val len= s.length / 2 val halfStr = s.substring(0, len) if(halfStr == s.substring(len)) Some(halfStr) else None } }
    • 也可能某个提取器模式不绑定任何变量,这时对应的 unapply 方法返回布尔值(true 表示成功,false 表示失败)。

      
      
      xxxxxxxxxx
      object UpperCase{ def unapply(s: String): Boolean = s.toUpperCase == s }

      这里仅定义了 unapply,并没有定义 apply。定义 apply 没有任何意义,因为没有任何东西需要构造。

  2. 如果我们希望匹配那些大写的、重复出现的子串。因此可以这样使用提取器:

    
    
    xxxxxxxxxx
    s math{ case Twice(x @ UpperCase()) => println("match :"+x) }

    这里有两点注意:

    • UpperCase() 的空参数列表不能省略,否则匹配的就是和 UpperCase 这个对象的相等性。
    • 尽管 UppserCase() 本身不绑定任何变量,但是我们仍然可以将跟它匹配的整个模式关联一个变量。做法是标准变量绑定机制:x @ UpperCase() 。这样的写法将变量 xUpperCase() 匹配的模式关联起来。

5.2 提取可变长度参数的模式

  1. 目前的 unapply 方法只支持固定长度的变量匹配。为支持可变长度的变量匹配,scala 允许我们定义另一个不同的 提取方法:unapplySeq

    
    
    xxxxxxxxxx
    object Domain{ def apply(parts: String*) : String = parts.reverse.mkString(".") def unapplySeq(str: String): Option[Seq[String]] = Some(str.split("\\.").reverse) }

    这里 unapplySeq 的结果类型必须符合 Option[Seq[T]] 的要求,其中元素类型 T 可以为任意 类型。

    另外,这里的 apply 方法也不是必须的。

  2. unapplySeq 返回“固定元素 + 可变部分” 也是可行的。这是通过将所有元素放在一个元组里面来实现的,其中可变部分放在元组最后:

    
    
    xxxxxxxxxx
    object Email{ def unapplySeq(email: String): Option[(String,Seq[String])] ={ val partes = email split "@" if(partes.length == 2) Some(partes(0), partes(1).split("\\.").reverse) else None } }

5.3 提取器和序列模式

  1. 可以使用序列模式来访问列表或数组的元素,如:

    
    
    xxxxxxxxxx
    List() List(x, y, _*) Array(x, 0, 0, _)

    事实上,Scala 标准库中的这些序列模式都是用提取器实现的。例如 List(...) 这样的模式之所以可行,是因为 scala.List 的伴生对象是一个定义了 unapplySeq 方法的提取器。

    
    
    xxxxxxxxxx
    package scala object List{ def apply[T](elems: T*) = elems.toList def unapplySeq[T](x: List[T]): Option[Seq[T]] = Some(x) }

    List 伴生对象包含了一个接收可变数量的入参的 apply 方法,正是这个方法让我们可以编写 List(), List(1, 2, 3) 这样的表达式。

    List 伴生对象还有一个以序列形式返回列表所有元素的 unapplySeq 方法。正是这个方法在支撑 List(...) 这样的模式。

    scala.Array 对象中我们也能够找到类似的定义,这些定义支持对数组的 injectionextraction

5.4 提取器 vs 样例类

  1. 样例类的缺点:样例类将数据的具体表现类型暴露给使用方。这意味着构造方法模式中使用的类名和选择器对象的具体表现类型相关。

    例如如下的模式匹配:

    
    
    xxxxxxxxxx
    a match{ case C(...) }

    如果匹配成功了,那么你就知道选择器表达式是 C 这个类的实例。

    提取器打破了数据表现和模式之间的关联:提取器支持的模式,和被选择的对象的数据类型没有任何联系。这个性质被称作表现独立 representation independence 。这很重要,因为它允许我们修改某些组件的实现类型,同时又不影响这些组件的使用方。

    如果你的组件定义了样例类,你就难以修改这些样例类,因为使用方可能已经包含了对这些样例类的模式匹配。重命名这些样例类,或者改变类的继承关系都会影响到使用方的代码。

    提取器没有这个问题,因为它们介于数据表现层和使用方看到的内容之间。你可以改变某个类型的具体表现形式,而不影响使用方的代码。

  2. 表现独立是提取器对于样例类的一个重要优势。另一方面,样例类也有一些相对于提取器的优势。

    • 首先,设置和定义样例类要比提取器简单得多,需要的代码也更少。
    • 其次,样例类比提取器能够带来更高效的模式匹配,因为 scala 编译器能够对使用样例类的模式做更好的优化。
    • 最后,如果你的样例类继承自一个 sealed 基类,那么 scala 编译器将检查你的模式匹配是否完整全面。如果有的值没有被覆盖到,则编译时会报错。对于提取器而言,并不存在这样的全面性检查。
  3. 至于选择样例类还是提取器,视具体情况而定。

    • 如果你编写的是一个封闭的应用,则样例类通常更好,因为它们精简、快速、还可以有静态检查。

      如果你之后决定需要修改你的类继承关系,则应用需要被重构。

    • 如果你需要将类型暴露给第三方,那么提取器可能是更好的选择,因为它们保持了表现独立。

    通常建议从样例类开始,随着需求变化再切换成提取器。

5.5 正则表达式

  1. 提取器的一个应用场景是正则表达式。和 Java 一样, scala 也通过类库提供了正则表达式的支持,不过提取器让我们更容易使用正则表达式。

  2. ScalaJava 继承了正则表达式语法,而Java 又从 Perl 继承了大部分的正则表达式特性。

  3. Scala 的正则表达式类位于 scala.util.matchingscala.util.matching.Regex 。创建一个正则表达式是将一个字符串传给 Regex 方法来创建的。如:

    
    
    xxxxxxxxxx
    import scala.util.matching.Regex val reg = new Regex("(-)?(\\d+)(\\.\\d*)?")

    这里 \ 出现多次。这是因为在 JavaScala 中,字符串中的单个反斜杠表示转义字符,而不是字符串中的常规字符。所以你需要写出 \\ 从而得到字符串里的一个 \ 字符。

  4. 如果正则表达式中有很多反斜杠,那么写起来和读起来都比较痛苦。可以使用 Scala 原生字符串来解决这个问题。

    Scala 原生字符串是由一对三个连续双引号括起来的字符序列,它和普通字符串的区别在于:原生字符串中的字符和它本身一样,没有转义。

    因此上述正则表达式也可以写成:

    
    
    xxxxxxxxxx
    val reg = new Regex("""(-)?(\d+)(\.\d*)?""")
  5. scala 中一种更短的创建正则表达式的方式是:

    
    
    xxxxxxxxxx
    val reg = """(-)?(\d+)(\.\d*)?""".r

    这是由于 StringOps 类有一个名为 .r 的方法,它将字符串转换为正则表达式。其定义为:

    
    
    xxxxxxxxxx
    package scala.runtime import scala.util.matching.Regex class StringOps(self: String)...{ ... def r = new Regex(self) }
  6. 有几种利用正则表达式查找的方法:

    • regex findFirstIn str:查找 str 中正则表达式首次出现的结果,结果为 Option 类型(找不到就是 None)。

      
      
      xxxxxxxxxx
      val reg = new Regex("""(-)?(\d+)(\.\d*)?""") reg findFirstIn "123.5 abc 45.6" // 返回 Option[String] = Some(123.5)
    • regex findAllIn str:查找 str 中正则表达式所有出现的结果,结果为Iterator 类型。

      
      
      xxxxxxxxxx
      val reg = new Regex("""(-)?(\d+)(\.\d*)?""") reg findFirstIn "123.5 abc 45.6" // 返回一个非空迭代器,迭代内容为 ["123.5", "45.6"]
    • findPrefixOf str:查找 str 以正则表达式开始(即 str 的头部)的结果,结果为 Option 类型(找不到就是 None)。

      
      
      xxxxxxxxxx
      val reg = new Regex("""(-)?(\d+)(\.\d*)?""") reg findFirstIn "de 123.5" // 返回 Option[String] = Some(123.5) reg findPrefixOf "de 123.5" // 返回 None reg findPrefixOf "123.5 de" // 返回 Option[String] = Some(123.5)
  7. scala 中每个正则表达式都定义了一个提取器。该提取器用来识别正则表达式中的分组(正则表达式中每个括号 () 代表一个分组)匹配的子字符串。

    例如:

    
    
    xxxxxxxxxx
    val reg = """(-)?(\d+)(\.\d*)?""".r val reg(sign, intPart, deciPart) = "-1.23" // sign 被绑定为 '-', intPart 被绑定为 '1', deciPart 被绑定为 '.23'

    在这里,模式 reg(...) 被用在了 val 的定义中。这里的 reg 正则表达式定义了一个 unapplySeq 方法,这个方法会匹配任何与 reg 匹配的字符串。

    • 如果字符串匹配了,那么正则表达式 reg 中的三个分组对应的部分就被作为模式的元素返回,进而被 sign, intPart, deciPart 绑定。

    • 如果某个分组缺失,则对应的元素值就被设为 null 。如:

      
      
      xxxxxxxxxx
      val reg = """(-)?(\d+)(\.\d*)?""".r val reg(sign, intPart, deciPart) = "1.23" // sign 被绑定为 null , intPart 被绑定为 '1', deciPart 被绑定为 '.23'
    • 如果无法匹配,则抛出 MatchError 异常。

      注意:这里要求字符串和正则表达式从头到尾完全匹配。这不同于 findFirstIn/findAllIn/findPrefixOf ,后者只需要字符串中部分匹配即可。

  8. 我们也可以在 for 表达式中混用提取器和正则表达式的find 。如下面的示例对字符串中找到的所有十进制数字进行拆解:

    
    
    xxxxxxxxxx
    val reg = """(-)?(\d+)(\.\d*)?""".r for (reg(s,i,d) <- reg.findAllIn(inputStr)) println("sign: %s, integer: %s, decimal: %s".format(s, i, d))

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文