第9章 泛型与隐式

类型参数与上下界、隐式参数与隐式类:这两块是读懂 Spark / Flink 源码的门槛,也是 Scala 最“难”的部分。

类型系统第13-14周2 讲4 段示例代码

本章学习目标

  • 会给类和函数加类型参数,并写上下界
  • 能看懂 Ordering 这类隐式参数的作用
  • 知道 Scala 3 用 given / using 取代了 implicit
  • 能把一段重复代码用泛型收敛

本章导览

核心内容

9.1

类型参数与上下界:泛型类与泛型方法

用类型参数写出可复用的泛型类和方法,并用上下界约束范围。

能定义泛型类和泛型方法,并让编译器推断类型实参能解释 <: 上界与 >: 下界分别限制什么能借助 Ordering 上下文界定写出可比较的泛型方法

知识点

类型参数写在方括号里

  • 类 Box[A] 的 A 在创建实例时确定,方法 def head[A](xs: List[A]): A 在调用时推断。
  • 泛型让容器和算法不绑定具体类型,同时保留编译期类型检查。
  • 调用泛型方法通常不必写 List[Int],编译器会从实参推断出 A。

上界 <: 与下界 >:

  • A <: Animal 表示 A 必须是 Animal 或它的子类,因此可以安全调用 Animal 的方法。
  • A >: Dog 表示 A 必须是 Dog 或它的父类型,适合把 Dog 放进更宽的集合。
  • 边界写错时常见报错是 type arguments do not conform to bounds。

上下文界定借来 Ordering

  • [A: Ordering] 等价于多一个隐式参数 (implicit ord: Ordering[A])。
  • List[Int].sorted 能工作,是因为作用域里有 Ordering.Int;自定义类型要自己提供。
  • JVM 会做类型擦除,运行时无法区分 List[Int] 和 List[String],别用 isInstanceOf 判断元素类型。

协变与逆变先认符号

  • Box[+A] 是协变:Box[Dog] 可以当 Box[Animal] 用,适合不可变容器。
  • Box[-A] 是逆变:方向反过来,函数参数位置常出现这种需求。
  • 初学先把默认的不变写对,确实需要放宽替换关系时再加 + 或 -。

代码示例

▶ 泛型类 Box 与泛型方法
SCALA
case class Box[A](value: A) {
  def map[B](f: A => B): Box[B] = Box(f(value))
  def pair[B](other: B): Box[(A, B)] = Box((value, other))
}

object GenericBox {
  def first[A](xs: List[A]): Option[A] = xs.headOption

  def middle[A](xs: List[A]): Option[A] =
    if (xs.size < 3) None else Some(xs(xs.size / 2))

  def main(args: Array[String]): Unit = {
    val b = Box(21).map(_ * 2)
    println(b)
    println(b.pair("answer"))
    println(first(List("a", "b")))
    println(middle(List(1, 2, 3, 4, 5)))
    println(first(List.empty[Double]))
  }
}
👀 运行结果
Box(42)
Box((42,answer))
Some(a)
Some(3)
None
▶ 上下界约束泛型范围
SCALA
abstract class Animal(val name: String) {
  def sound: String
}

class Dog(name: String) extends Animal(name) {
  def sound = "Woof"
}

class Cat(name: String) extends Animal(name) {
  def sound = "Meow"
}

object BoundsDemo {
  def introduce[A <: Animal](a: A): String = s"${a.name} 说 ${a.sound}"

  def addDog[A >: Dog](xs: List[A], dog: Dog): List[A] = dog :: xs

  def main(args: Array[String]): Unit = {
    println(introduce(new Dog("旺财")))
    println(introduce(new Cat("咪咪")))

    val animals: List[Animal] = List(new Cat("咪咪"))
    val more = addDog(animals, new Dog("旺财"))
    println(more.map(_.name))

    println(addDog(List(new Dog("A")), new Dog("B")).map(_.name))
  }
}
👀 运行结果
旺财 说 Woof
咪咪 说 Meow
List(旺财, 咪咪)
List(B, A)

常见错误

⚠️ 踩坑与解决
  • 给 A <: Animal 传了 String
    → 上界已经把可选类型缩小到 Animal 族,String 不满足约束,编译期就拒绝。;要么传 Animal 的子类,要么去掉上界、换成不依赖 Animal 能力的类型参数。
  • 在 A >: Dog 的方法体里调用 a.sound
    → 下界只保证 A 比 Dog 宽,A 可能是 Animal 甚至 Any,编译器不知道它有 sound。;要使用 Dog 的能力应写 A <: Dog;下界是用来把 Dog 装进更宽的集合。
  • 用 isInstanceOf[List[Int]] 判断元素类型
    → JVM 类型擦除后 List[Int] 和 List[String] 运行时都是 List,判断永远为真。;需要区分时携带 ClassTag,或在数据模型里显式放一个类型标签。

💡 先写具体类型的版本,再把重复部分换成 A;上界决定“能调用什么”,下界决定“能装进什么”。

9.2

implicit:隐式参数、隐式类与 Scala 3 的 given

理解隐式参数与隐式类,知道 Scala 3 的 given/using 写法。

能写隐式参数,让编译器自动注入 Ordering 或自定义能力能用 implicit class 给现有类型增加方法能说出 implicit、given/using 与上下文界定的对应关系

知识点

隐式参数是编译器补上的参数

  • def top[A](xs: List[A])(implicit ord: Ordering[A]): A 调用时只传 xs。
  • 编译器在当前作用域找类型匹配的 implicit 值,找到就自动填入最后一个参数列表。
  • 找不到时会报 could not find implicit value,通常说明缺实例或忘 import。

上下文界定是语法糖

  • [A: Ordering] 会展开成隐式参数列表,方法体里用 implicitly[Ordering[A]] 取出实例。
  • Ordering、ClassTag 这类 type class 都爱用上下文界定,让签名保持干净。
  • 自定义 type class 也可以用 implicit val 或 implicit object 提供实例。

隐式类给现有类型加方法

  • implicit class 必须定义在 object、class 或 trait 里,不能裸放在包顶层。
  • 导入后,"abc".shout 会被编译器改写成 RichString("abc").shout。
  • 隐式类构造器只能有一个非隐式参数,而且不能是 case class。

Scala 3 的写法差异

  • Scala 3 用 given/using 取代隐式定义与隐式参数,并支持省略花括号的缩进语法。
  • 本课统一按 Scala 2.13 的 implicit 与花括号写法提交,不要把两种风格混在一起。

代码示例

▶ 隐式参数与自定义 Ordering
SCALA
case class User(name: String, age: Int)

object ImplicitParamDemo {
  implicit val userOrdering: Ordering[User] =
    Ordering.by(u => (u.age, u.name))

  def top[A](xs: List[A])(implicit ord: Ordering[A]): A = xs.max

  def sorted[A: Ordering](xs: List[A]): List[A] = xs.sorted

  def main(args: Array[String]): Unit = {
    val users = List(User("张三", 20), User("李四", 22), User("王五", 22))
    println(top(users))
    println(sorted(users).map(_.name))
    println(top(List(3, 9, 2)))
    println(sorted(List("pear", "apple", "fig")))
  }
}
👀 运行结果
User(王五,22)
List(张三, 李四, 王五)
9
List(apple, fig, pear)
▶ 隐式类扩展现有类型
SCALA
object RichDemo {
  implicit class RichString(private val s: String) {
    def shout: String = s.toUpperCase + "!"
    def words: List[String] = s.split("\\s+").toList.filter(_.nonEmpty)
  }

  implicit class RichInt(private val n: Int) {
    def repeated[A](f: Int => A): List[A] = (1 to n).map(f).toList
  }

  def main(args: Array[String]): Unit = {
    println("hello scala".shout)
    println("a b  c".words)
    println(3.repeated(i => i * i))
  }
}
👀 运行结果
HELLO SCALA!
List(a, b, c)
List(1, 4, 9)

常见错误

⚠️ 踩坑与解决
  • 调用 top(users) 报 could not find implicit value
    → 作用域里没有 Ordering[User],或隐式实例放在别的 object 却没有 import。;把实例放进伴生对象让编译器自动查找,或在调用处显式 import。
  • 把 implicit class 放在包顶层
    → Scala 2 顶层不能直接放 implicit class,它必须是 object、class 或 trait 的成员。;把隐式类包进 object,再 import 该对象的成员。

💡 隐式不是魔法,而是让编译器替你传参;报错时先把类型写全,缺哪个实例通常就一目了然。