Scala 3中ValueOf/valueOf机制解析及类参数值问题排查
问题背景
我尝试给类指定一个默认值参数,编写了以下代码:
import scala.collection.mutable class MyMap[K,V,D <: V] { private val impl = mutable.Map[K, V]() def apply(k:K):V = impl.getOrElse(k, valueOf[D]) // ^^^^^^^^^^ 报错 def put(k:K,v:V):Unit = impl += k -> v } val m = MyMap[Int, Int, 0]() m(100) // 预期返回0
这段代码无法运行,但给apply方法添加一个看似无意义的隐式参数后,代码就能正常工作:
import scala.collection.mutable class MyMap[K,V,D <: V] { private val impl = mutable.Map[K, V]() def apply(k:K)(implicit unusedName:ValueOf[D]):V = impl.getOrElse(k, valueOf[D]) def put(k:K,v:V):Unit = impl += k -> v } val m = MyMap[Int, Int, 0]() m(100) // 正常返回0
我尝试显式传递隐式参数,结果却不符合预期:
scala> m(2)(0) -- [E007] 类型不匹配错误: ---- 1 |m(2)(0) | ^ | 找到: (0 : Int) | 需要: ValueOf[(0 : Int)] scala> m(2)(ValueOf[0]) -- 错误: -------------------------- 1 |m(2)(ValueOf[0]) | ^^^^^^^^^^ | 类ValueOf的构造函数缺少参数 scala> m(2)(ValueOf[0](0)) val res7: Int = 0
核心问题
ValueOf/valueOf的工作机制是什么?- 为何直接使用
valueOf[D]无法生效?逻辑上这应该可行才对。
更新补充的可行代码片段
评论中提到的两种正确实现方式:
代码片段1
import scala.collection.mutable final class MyMap[K, V] private (default: V) { private val impl = mutable.Map.empty[K, V] def apply(k: K): V = impl.getOrElse(k, default) def put(k: K,v: V): Unit = { impl += k -> v } } object MyMap { def empty[K, V, D <: V](using ValueOf[D]): MyMap[K, V] = new MyMap(default = valueOf[D]) } val m = MyMap.empty[Int, Int, 0] m(100)
代码片段2
import scala.collection.mutable final class MyMap[K, V, D <: V](using ValueOf[D]) { private val default = valueOf[D] private val impl = mutable.Map.empty[K, V] def apply(k: K): V = impl.getOrElse(k, default) def put(k: K,v: V): Unit = { impl += k -> v } } val m = MyMap[Int, Int, 0]() m(100)
解答
1. ValueOf/valueOf的工作机制
ValueOf[T]是Scala提供的类型类,作用是将单例类型(比如具体值0对应的类型)与它的实际值绑定。当你使用ValueOf[0]时,Scala编译器会自动生成一个ValueOf[0]的隐式实例,这个实例内部持有0这个具体值。
valueOf[T]是语法糖,它会在当前作用域中查找ValueOf[T]的隐式实例,然后取出实例中持有的值,本质等价于summon[ValueOf[T]].value。
简单来说:ValueOf是承载值的容器,valueOf是从容器中取值得快捷方式,这一切都依赖编译器能找到对应的隐式实例。
2. 为何直接使用valueOf[D]无法生效?
在第一段代码中,class MyMap[K,V,D <: V]的类型参数D只是普通类型参数,编译器无法确定D是单例类型,更无法自动为它生成ValueOf[D]的隐式实例。当你在apply方法中写valueOf[D]时,编译器需要找到ValueOf[D]的隐式实例,但此时作用域中没有该实例——因为你没有给编译器添加约束:D必须是能提供ValueOf[D]的类型。
而给apply方法添加implicit unusedName:ValueOf[D]参数时,相当于给编译器加了约束:只有当存在ValueOf[D]的隐式实例时,该方法才能被调用。此时调用m(100)时,编译器会自动为D=0生成对应的ValueOf[0]隐式实例并传入,valueOf[D]就能找到实例并取出值。
关于显式传参出错的原因:
m(2)(0)错把Int值当成ValueOf[0]类型,类型不匹配;ValueOf[0]是类型而非实例,不能直接传递;ValueOf[0](0)是手动创建实例,虽然能工作,但完全没必要——编译器原本可以自动生成该实例。
关于两种更新代码的说明
这两种实现的核心思路都是将ValueOf[D]的隐式约束提前到编译器能推导的位置:
- 代码片段1:把约束放在伴生对象的工厂方法
empty上,调用MyMap.empty[Int,Int,0]时,编译器生成ValueOf[0]实例,将值传入类构造函数,类内部直接使用该值即可。 - 代码片段2:把约束放在类的构造函数上(Scala 3的
using写法),创建MyMap实例时,编译器自动注入ValueOf[D]实例,类内部通过valueOf[D]取出值。
这两种写法都比最初在apply方法中加约束更合理,因为默认值是类的属性,应在类初始化时确定,而非每次调用apply时才查找隐式实例。
内容的提问来源于stack exchange,提问作者18446744073709551615

