概览
这几个特性——协变、逆变、上界/下界、隐式参数、隐式转换——是 Scala 类型系统与抽象能力的“锋刃”。
协变与逆变
协变/逆变回答的是“类型参数随子类型关系如何变化”的问题。记号:+T
协变,-T
逆变,不标注则不变(invariant)。
- 协变
+T
(只能‘产出’)- 定义:若
A
是B
的子类型,则Container[A]
是Container[B]
的子类型。 - 典型:不可变容器、只读数据源。
- 标准库例子:
List[+A]
、Option[+A]
、Either[+A, +B]
、Function1[-T, +R]
(返回值协变)。 - 约束:协变类型参数不能出现在方法参数位置(否则会破坏类型安全)。
- 定义:若
// 只读源:产出 A,安全协变
trait Source[+A] {
def next(): A
}
// 错误示例:协变类型 A 出现在入参位置
// class Box[+A](var value: A) { def set(a: A): Unit = value = a } // 编译失败
- 逆变
T
(只能‘消费’)- 定义:若
A
是B
的子类型,则Handler[B]
是Handler[A]
的子类型。 - 典型:回调、比较器、写入端。
- 标准库例子:
Ordering[-T]
、CanEqual[-L, -R]
、函数参数Function1[-T, +R]
(参数逆变)。
- 定义:若
// 只写汇:消费 A,安全逆变
trait Sink[-A] {
def put(a: A): Unit
}
val animalSink: Sink[Animal] = ???
val dogSink: Sink[Dog] = animalSink // 逆变允许将“更泛”的 Sink[Animal] 用作 Sink[Dog]
- 不变(默认)
- 当类型参数既作为入参又作为出参,或带有可变状态时,保持不变最安全。
- 标准库例子:
Array[T]
在 Scala 中是不变的(避免 Java 协变数组的运行期类型错误)。
API 设计范式
- **只读(生产者)**用协变:
Source[+A]
、Stream[+A]
- **只写(消费者)**用逆变:
Sink[-A]
、Handler[-E]
、Ordering[-T]
- 读写或可变状态保持不变:
Buffer[T]
、Array[T]
上界与下界
类型参数的“围栏”。上界收紧“最多到哪”,下界收紧“至少从哪”。
- 上界
<:
(子类型约束)- 语义:
T <: U
表示T
必须是U
的子类型。 - 用途:限制操作依赖于超类型能力,如序列可迭代、元素可比较。
- 语义:
// 只接受可迭代集合
def firstOf[T <: Iterable[A], A](xs: T): Option[A] = xs.headOption
- 下界
>:
(父类型约束)- 语义:
T >: L
表示T
必须是L
的超类型。 - 用途:与协变类型配合,提供“向上扩容”的返回类型,保持类型安全(经典 trick)。
- 语义:
// 协变容器的安全写入:返回更“泛”的容器
final case class Box[+A](get: A) {
def map[B >: A](f: A => B): Box[B] = Box(f(get))
}
val b1: Box[Dog] = Box(new Dog)
val b2: Box[Animal] = b1.map(identity) // B >: Dog,返回 Box[Animal]
- 组合示例:PECS(Producer Extends, Consumer Super)的 Scala 化
- 生产者(只读):常用上界保证能力,如
T <: Animal
。 - 消费者(只写):常用下界扩容返回类型或接受更泛类型,如
B >: Cat
。
- 生产者(只读):常用上界保证能力,如
隐式参数与类型类
Scala 把“上下文能力”以隐式参数的方式传递;类型类是最常见的模式。Scala 2 和 Scala 3 语法不同,语义一致。
Scala 2
// 类型类
trait Show[T] { def show(x: T): String }
// 实例(放在伴生对象便于自动搜索)
object Show {
implicit val intShow: Show[Int] = (x: Int) => s"#${x}"
}
// 使用:隐式参数 or 上下文界定
// 直接声明一个隐式参数 ev: Show[T]
// 更灵活,例如如果你需要同时使用多个隐式参数(如 implicit ev1: A[T], ev2: B[T]),这种写法更直接。
def printIt[T](x: T)(implicit ev: Show[T]): String = ev.show(x)
// T: Show 是一个上下文界定,表示要求存在一个 Show[T] 类型的隐式值。
// 适用于只需要一个隐式参数的情况(多个隐式参数时不如直接写隐式参数列表方便)。
def printIt2[T: Show](x: T): String = implicitly[Show[T]].show(x)
printIt2(42) // "#42"
Scala 3
trait Show[T]:
def show(x: T): String
object Show:
given Show[Int] with
def show(x: Int) = s"#$x"
// 上下文参数 using + summon
def printIt[T](x: T)(using ev: Show[T]): String = ev.show(x)
def printIt2[T: Show](x: T): String = summon[Show[T]].show(x)
- 上下文界定
T: C
等价于额外的隐式参数(implicit ev: C[T])
(Scala 2)或(using C[T])
(Scala 3)。 - 隐式搜索范围:局部作用域、导入作用域、目标类型的伴生对象、类型类的伴生对象。将实例放到伴生对象是最佳实践。
- 歧义与优先级:多个候选会报歧义。通过“低优先级”父特质分层提供缺省实例,子特质给更具体实例。
// 低优先级技巧(Scala 2)
trait LowPriOrdering {
implicit def tupleOrd[A: Ordering, B: Ordering]: Ordering[(A,B)] =
Ordering.by[(A,B), (A,B)](identity)
}
object MyOrderings extends LowPriOrdering {
implicit val stringOrd: Ordering[String] = Ordering.by(_.length) // 更高优先级
}
常见类型类场景
- 排序/比较:
Ordering[T]
、Equiv[T]
- 序列化/编解码:
Encoder[T]
/Decoder[T]
(如 JSON、Avro) - 数值代数:
Numeric[T]
、Fractional[T]
- 证据参数:
=:=
(相等)、<:<
(子类型)、=:!=
(不相等,社区实现)
// 证据约束:仅当 T 是 Seq[_] 时可调用
def onlySeq[T](x: T)(implicit ev: T <:< Seq[_]): Int = x.length
隐式转换与扩展方法
隐式转换能把“不会”的对象变成“会”的对象,但滥用会制造幽灵行为。Scala 3 强化了约束,推荐首选扩展方法。
- 扩展方法(首选方式)
- Scala 2:
implicit class
(仅一参构造,值类可零开销) - Scala 3:
extension
语法更直观
- Scala 2:
// Scala 2
implicit class RichString(private val s: String) extends AnyVal {
def snake: String = s.replaceAll("([A-Z])", "_$1").toLowerCase
}
// Scala 3
extension (s: String)
def snake: String = s.replaceAll("([A-Z])", "_$1").toLowerCase
- 隐式转换(谨慎使用)
- Scala 2:
implicit def fromA(a: A): B
- Scala 3:需显式引入
scala.Conversion
并用given Conversion[A,B]
- 仅在“语义等价、无歧义、局部生效”的场景使用,如与外部 API 小缝合。
- Scala 2:
// Scala 3
import scala.util.chaining.*
import scala.language.implicitConversions
import scala.Conversion
final case class Cents(value: Long)
given Conversion[Int, Cents] with
def apply(x: Int): Cents = Cents(x.toLong)
// 小心:任何 Int 可被当作 Cents 使用
def pay(c: Cents): Unit = ()
pay(199) // 发生隐式转换
- 视图界定
<%
已废弃:用T: SomeTypeClass
或given Conversion
替代。
应用场景与完整案例
不可变集合与生产/消费分离
- 目标:对外只读、内部可扩容;写入返回更泛类型,保证协变安全。
sealed trait Event
final case class UserJoined(id: String) extends Event
final case class UserLeft(id: String) extends Event
final case class Events[+E](private val data: Vector[E]) {
def ::[B >: E](e: B): Events[B] = Events(e +: data) // 下界扩大
def foreach(f: E => Unit): Unit = data.foreach(f)
}
val ev0: Events[UserJoined] = Events(Vector(UserJoined("u1")))
val ev1: Events[Event] = UserLeft("u1") :: ev0 // 返回 Events[Event]
比较与排序的类型类化
- 目标:语义可插拔;局部重定义不影响全局。
final case class Person(name: String, age: Int)
// 缺省:按年龄
object Person:
given ageOrd: Ordering[Person] = Ordering.by(_.age)
// 本地:按名字长度
def topKByNameLen(ps: List[Person], k: Int)(using ord: Ordering[Person]): List[Person] =
ps.sorted(ord.reverse).take(k)
{
given Ordering[Person] = Ordering.by(_.name.length)
val r = topKByNameLen(List(Person("Ann", 30), Person("Elizabeth", 28)), 1)
// 使用本地 given,而非伴生对象里的 ageOrd
}
安全回调与处理器(逆变)
- 目标:让更“泛”的处理器能处理更“细”的事件。
trait Handler[-E]:
def handle(e: E): Unit
def on[E](h: Handler[E], e: E) = h.handle(e)
val anyEventHandler: Handler[Event] = (e: Event) => println(e)
val left = UserLeft("u1")
on(anyEventHandler, left) // 逆变成立
JSON 编解码(类型类 + 上下文界定)
- 目标:无侵入编解码,自动衍生。
trait Encoder[T] { def toJson(x: T): String }
object Encoder:
given Encoder[Int] with
def toJson(x: Int) = x.toString
given [A](using enc: Encoder[A]): Encoder[List[A]] with
def toJson(xs: List[A]) = xs.map(enc.toJson).mkString("[", ",", "]")
def asJson[T: Encoder](x: T): String = summon[Encoder[T]].toJson(x)
asJson(List(1,2,3)) // "[1,2,3]"