Scala 2 的 implicit 一词四用(隐式转换、隐式参数、隐式类、隐式对象)是全语言最被诟病的设计;Scala 3 把它拆成 given(提供实例)与 using(请求实例),并让隐式转换显式声明。这一节讲清新体系,也帮你看懂老代码。
先看"手动版":解析 JSON 需要一个解码器,每个调用点都亲手传:
def decode[A](raw: String, dec: Decoder[A]): Option[A] = ??? decode[User]("""{"id":1}""", userDecoder) // 每次都要带上
using 版:把"证据"标记为可省,编译器按类型去找:
trait Decoder[A]: def decode(raw: String): Option[A] given Decoder[Int] with def decode(raw: String): Option[Int] = raw.toIntOption given userDec: Decoder[User] with def decode(raw: String): Option[User] = ??? def decode[A](raw: String)(using dec: Decoder[A]): Option[A] = dec.decode(raw) decode[Int]("42") // 编译器找到 Int 的 given decode[User]("""{"id":1}""") // 命中 userDec
关键理解:given 不是魔法注入,是"按类型索引的自动传参"。运行时和你手写传参完全一样,所有决策发生在编译期。这也解释了老 implicit 最大的痛点——出问题时你不知道编译器从哪个角落找到了哪个实例。Scala 3 的改进正是围绕这里:given 不再被通配 import 意外带进来,作用域规则收紧,还有 -source:future 的显式导入检查。
def decode[A: Decoder](raw: String): Option[A] = summon[Decoder[A]].decode(raw)
[A: Decoder] 展开就是 (using Decoder[A]),summon 把编译器找到的实例取出来。上下文边界让签名一行写完,标准库与 Cats 等库大量使用。
编排建议:类型自己的 given 放它的伴生对象(全局默认),需要换实现时在局部作用域再给一个(就近覆盖)。6.1 的 Future 需要 ExecutionContext,就是这套机制的日常应用。
Scala 2 里 implicit def strToId(s: String): Id 能让 String 悄悄变 Id——方便但会链式引爆,读代码的人完全看不见转换发生。Scala 3 要求:
import language.implicitConversions // 显式打开管制区 given Conversion[String, Id] with def apply(s: String): Id = Id(s)
必须显式 import 才启用,且转换定义集中可见。我的实践红线:隐式转换只用于与 Java 集合/旧 API 的边界缝合(7.2 会用到),业务类型之间一律显式转换。
隐式类(Scala 2 给已有类型加扩展方法)在 Scala 3 有了直白替代:
extension (s: String) def toId: Id = Id(s) "abc".toId
given 提供证据、using 消费证据,中间的查找过程值得在 REPL 里盯一遍:
trait Ord[A]: def compare(x: A, y: A): Int given Ord[Int] with def compare(x: Int, y: Int) = x - y def max[A](a: A, b: A)(using ord: Ord[A]): A = if ord.compare(a, b) >= 0 then a else b max(3, 9) // 9:编译器在作用域内找到 Ord[Int] 的 given,自动填入 max("a", "b") // 编译错误:no given instance of Ord[String]
第二次调用的报错就是隐式机制的全部秘密:编译器按"当前作用域、伴生对象、显式导入"的顺序找类型为 Ord[String] 的 given,找不到就拒绝编译。Scala 2 时代同样的机制由 implicit def 与 implicit 参数实现,查找规则更晦涩、报错更难读;Scala 3 把"提供"与"消费"拆成两个关键词,语义分工一目了然。
def max2[A: Ord](a: A, b: A): A = val ord = summon[Ord[A]] // 上下文边界 + summon 取回证据 if ord.compare(a, b) >= 0 then a else b
[A: Ord] 读作"A 附带一个 Ord 证据",等价于 using 参数但不出现在参数表。标准库与 Cats 的签名大量使用这种写法,读源码时认不出它就没法继续往下读。
当作用域里有两个同类型 given,编译器报 ambiguous。处置顺序:先查是否误导入了两个库的默认实例;再考虑显式 using 传参绕过查找;最后用 given 的命名优先级(局部覆盖导入)收窄。这类报错在混用 Cats 与标准库排序时最常见。
[A: Decoder] 是 using 参数的签名糖。