Scala隐式类:不修改源码也能扩展类功能

59 阅读6分钟

一、导入

在Scala中,我们经常会遇到这样的场景:一个现有的类功能不够用,但又不能直接修改它的源代码(比如是第三方库的类)。传统的做法是通过继承或装饰器模式来扩展功能,但Scala提供了一种更优雅的解决方案——隐式类。

通过隐式类,我们可以在不修改原有类代码的情况下,为其添加新的方法。这就像是给现有的类"注射"了新功能,让代码扩展变得更加灵活和简洁。

二、讲授新课

(一)隐式类的概念

隐式类是Scala中一种特殊的类,使用implicit关键字修饰。它的主要作用是在不修改原有类源代码的情况下,为该类添加新的方法。

定义语法

implicit class 类名(参数: 原有类型) {
    // 新增的方法
}

工作原理

  1. 编译器发现对一个对象调用了它没有的方法
  2. 在当前作用域中查找是否存在能将该对象转换为具有该方法的类型的隐式转换
  3. 如果找到隐式类,自动将原对象转换为隐式类的实例
  4. 调用隐式类中的方法

(二)隐式类的基本使用

案例:给User类添加updateUser方法

代码

package imp

/*
*目标: 已有一个写好的类, 要求在不修改这个类的源代码的情况下, 拓展这个类的新功能!
*思路:
* 1. 补充定义一个新类, 在这个新类中提供新方法;
* 2. 提供一个隐式转换函数, 把之前旧类对象转换成这个新类对象。
* */
object imp04 {

  class User() {
    def insertUser(): Unit = {
      println("insertUser......")
    }
  }

  class UserStrong() {
    def updateUser(): Unit = {
      println("updateUser......")
    }
  }

  implicit def xxxxx(user:User):UserStrong = {
    println("自动调用隐式转换函数.......")
    new UserStrong
  }

  def main(args: Array[String]): Unit = {
    val u1 = new User()
    u1.insertUser()
    // val u2 = new UserStrong()
    u1.updateUser()
    // u2.updateUser()
  }
}

代码结果

4.png 代码分析

  1. 定义了一个User类,只有一个insertUser方法
  2. 定义了一个UserStrong类,包含updateUser方法
  3. 定义了一个隐式转换函数xxxxx,将User对象转换为UserStrong对象
  4. 当调用u1.updateUser()时,编译器发现User类没有这个方法
  5. 自动查找并调用隐式转换函数,将u1转换为UserStrong对象
  6. 然后调用转换后对象的updateUser方法

这种方法虽然可行,但需要定义两个类和一个转换函数,比较繁琐。隐式类可以简化这个过程:

代码

package imp

/*
*目标: 已有一个写好的类, 要求在不修改这个类的源代码的情况下, 拓展这个类的新功能!
*思路:
* 1. 补充定义一个新类, 在这个新类中提供新方法;
* 2. 提供一个隐式转换函数, 把之前旧类对象转换成这个新类对象。
*
* 隐式类。
* implicit class
*
* 作用:在不修改原来类的基础之后,增加新的功能
* */
object imp04bu {

  class User() {
    def insertUser(): Unit = {
      println("insertUser......")
    }
  }

  implicit class UserStrong(user:User) {
    def updateUser(): Unit = {
      println("updateUser......")
    }
  }

  def main(args: Array[String]): Unit = {
    val u1 = new User()
    u1.insertUser()
    // val u2 = new UserStrong()
    u1.updateUser()
    // u2.updateUser()
  }
}

代码结果

4bu.png 代码分析

  1. 使用隐式类UserStrong替代了之前的UserStrong类+隐式转换函数
  2. 隐式类自动提供了从UserUserStrong的转换
  3. 代码更加简洁,逻辑更清晰
  4. 调用方式完全一样,但实现更优雅

(三)隐式类的实际应用场景

隐式类最常见的应用场景是为现有的类(特别是Scala标准库或第三方库的类)添加实用方法。

案例1:为String类添加手机号验证功能

代码

package imp

/*
* 目标: 让任意一个字符串具备一个功能: 判断是否是一个合法的手机号。
* String类是系统提供的, 并没有isPhone这个方法。
* 现在就要去在不修改String类的情况下, 增加这个方法。
* */
object imp05 {

  implicit class StrongString(s: String) {
    def isPhone: Boolean = {
      val reg = "^[135678]\\d{9}$".r
      reg.matches(s)
    }

    def isIDCard: Boolean = {
      val reg = "^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[1-2]\\d|3[0-1])\\d{3}[0-9Xx]$".r
      reg.matches(s)

    }
  }

  def main(args: Array[String]): Unit = {
    val str = new String("13617295643")
    // val str = "13617295643"
    // str.isPhone // 判断自己是否是一个合法的手机号。返回值是boolean。true,false
    println(str.isPhone)
    println("134567891a".isPhone)
    println("130567891a".isPhone)
    println("429005202011012231".isIDCard) // true
    println("40900520201101223a".isIDCard) // false
  }
}

代码结果

5.png 代码分析

  1. 定义了一个隐式类StrongString,包装了String类型
  2. 添加了isPhone方法,使用正则表达式验证手机号格式
  3. 添加了isIDCard方法,验证身份证号码格式
  4. 任何String对象都可以直接调用这些方法,就像它们原本就存在一样
  5. 正则表达式:
    • 手机号:以1、3、5、6、7、8开头,后面跟9位数字
    • 身份证:符合中国身份证号码格式规则

案例2:为Int类添加阶乘计算功能

代码

package imp

import scala.language.postfixOps

/*
* 目标: 让任意一个整数具备一个功能: 计算阶乘。
* n! = 1×2×3×...×n
* 5! = 1 * 2 * 3 * 4 * 5 = 120
* */
object imp06 {
  implicit class StrongInt(n: Int) {
    def ! : Int = {
      var rst = 1
      for (i <- 1 to  n){
        rst *= i
      }
      rst
  }
}

def main(args: Array[String]): Unit = {
  println(4 !) // 24
  println(5 !) // 120
  }
}

代码结果

6.png 代码分析

  1. 定义了一个隐式类StrongInt,包装了Int类型
  2. 添加了!方法,计算整数的阶乘
  3. 由于!是后缀操作符,需要导入scala.language.postfixOps
  4. 阶乘计算使用循环实现:n! = 1 × 2 × 3 × ... × n
  5. 任何整数都可以直接使用!操作符计算阶乘

(四)隐式类的组织与管理

当项目中隐式类较多时,良好的组织和管理很重要。通常的做法是将隐式类放在单独的文件或包对象中。

案例:集中管理隐式类

代码

package imp

object imps {
  implicit class strongInt(n: Int) {
    def ! : Int = {
      var rst = 1
      for (i <- 1 to n) {
        rst *= i
      }
      rst
    }
  }

  implicit class StrongString(s: String) {
    def isPhone: Boolean = {
      val reg = "^[135678]\\d{9}$".r
      reg.matches(s)
    }

    def isIDCard: Boolean = {
      val reg = "^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$".r // 身份证号码的正则表达式
      reg.matches(s)
    }
  }
}

使用代码

package imp
import scala.language.postfixOps

import imp.imps._

object imp07 {

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

    println(4!) // 24
    println(5!) // 120

    println("13614254546".isPhone)

  }
}

代码结果

7.png

代码分析

  1. 将所有的隐式类集中定义在imps对象中
  2. 在其他文件中使用时,只需导入import imp.imps._
  3. 这种组织方式使得隐式类的管理更加清晰
  4. 便于团队协作和代码维护
  5. 可以根据功能将隐式类分组到不同的对象中

三、隐式类的使用限制

  1. 只能定义在类、特质或对象内部:不能定义在顶级作用域
  2. 主构造函数必须有且只有一个参数:这个参数类型就是要扩展的类型
  3. 不能是样例类(case class)
  4. 作用域内不能有相同名称、相同参数类型的隐式类
  5. 隐式类不能直接定义方法重载