Scala协变、逆变、上界/下界、隐式参数、隐式转换

概览

这几个特性——协变、逆变、上界/下界、隐式参数、隐式转换——是 Scala 类型系统与抽象能力的“锋刃”。


协变与逆变

协变/逆变回答的是“类型参数随子类型关系如何变化”的问题。记号:+T 协变,-T 逆变,不标注则不变(invariant)。

  • 协变 +T(只能‘产出’)
    • 定义:若 AB 的子类型,则 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(只能‘消费’)
    • 定义:若 AB 的子类型,则 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
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 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: SomeTypeClassgiven 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]"



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值