总结评论并解释这个编解码器是如何工作的。
当你这样做时:
def something[T: ConfigReader] = ...
您正在使用语法糖
// Scala 2
def something[T](implicit configReader: ConfigReader[T]) = ...
// Scala 3
def something[T](using configReader: ConfigReader[T]) = ...
在您编写时在呼叫站点上:
something[T]
编译器实际上做了
something(configReaderForT /* : ConfigReader[T] */)
所以基本上它是编译器支持的基于类型的依赖注入。并且依赖注入必须从某个地方获取要传递的值。
编译器如何获取该值以传递它?它必须通过它在作用域中的类型来找到它。应该有一个标记为(Scala 2) 或(Scala 3)def
的此类最接近的值(或返回此值)。implicit
given
// Scala 2
implicit val fooConfigReader: ConfigReader[Foo] = ...
something[Foo] // will use fooConfigReader
// Scala 3
given fooConfigReader: ConfigReader[Foo] = ...
something[Foo] // will use fooConfigReader
Scala 3 基本上使区分哪个是价值的定义更容易 - given
- 哪个是依赖于从外部某个地方提供价值的地方 - using
。Scala 2 有一个词来形容它——implicit
这引起了很多混乱。
您必须自己定义此值/方法或将其导入 - 在需要它的范围内 - 否则编译器只会尝试查看对您的类型有贡献的所有类型的伴随对象T
- 如果T
是特定的。(或者如果它无法在编译器错误消息中的任何地方找到它,则失败)。
// Example of companion object approach
// This is type Foo
case class Foo()
// This is Foo's companion object
object Foo {
// This approach (calling derivation manually) is called semiauto
// and it usually needs a separate import
import pureconfig.generic.semiauto._
implicit val configReader: ConfigReader[Foo] = deriveReader[Foo]
}
// By requiring ConfigReader[Foo] (if it wasn't defined/imported
// into the scope that needs it) compiler would look into:
// * ConfigReader companion object
// * Foo companion object
// ConfigReader doesn't have such instance but Foo does.
如果T
是通用的,那么您必须将该implicit
/given
作为参数传递 - 但是您只是推迟了必须指定它并让编译器找到/生成它的时刻。
// Tells compiler to use value passed as parameter
// as it wouldn't be able to generate it based on generic information
// implicit/using expressed as "type bounds" (valid in Scala 2 and 3)
def something[T: ConfigReader] = ...
// Scala 2
def something[T](implicit configReader: ConfigReader[T]) = ...
// Scala 3
def something[T](using configReader: ConfigReader[T]) = ...
// It works the same for class constructors.
在 PureConfig 的情况下,pureconfig.generic.auto
containsimplicit def
为指定的 生成值T
。如果要生成它,则必须将其导入到需要该特定实例的位置。您可以在伴随对象中执行此操作,以使其在ConfigReader
此特定类型需要的任何地方自动导入,或将其导入 main(或任何其他指定 T 的地方)。一种或另一种方式,您将不得不在某处派生它,然后将其添加[T: ConfigReader]
或添加到(implicit configReader: ConfigReader[T])
不应硬编码T
到任何东西的所有方法的签名中。
总结您的选择是:
- 将导入保留在 main 中(如果您在 main 中硬编码
T
为特定类型)
- 派生它并定义为
implicit
其他地方,然后从那里导入它(有些人在trait
s 中做它然后混合它们,但我不喜欢这种方法)
- 派生它并定义为
implicit
伴生对象
只要您希望您的配置是解析值而不是非类型化 JSON (HOCON) 而无需自己编写这些编解码器,您就必须在某处执行自动(或半自动)派生。