在隐式类中创建的Scala不兼容嵌套类型

Iva*_*ass 5 scala implicit inner-classes scala-2.12

所提供的代码片段是一个虚构的简约示例,仅用于说明问题,与实际的业务逻辑类型无关。

在下面的代码中,我们在Entry类型内部有一个嵌套Registry类型。

class Registry[T](name: String) {
  case class Entry(id: Long, value: T)
}
Run Code Online (Sandbox Code Playgroud)

这是有道理的,因为不同注册表的条目是不同的,不可比拟的类型。

然后,我们可能有一个隐式的Ops类,例如,在测试中使用的类,它将我们的注册表绑定到某些测试存储实现(一个简单的可变映射)

object testOps {

  import scala.collection.mutable

  type TestStorage[T] = mutable.Map[Long, T]

  implicit class RegistryOps[T](val self: Registry[T])(
    implicit storage: TestStorage[T]
  ) {

    def getById(id: Long): Option[self.Entry] = 
      storage.get(id).map(self.Entry(id, _))

    def remove(entry: self.Entry): Unit       = storage - entry.id
  }
}
Run Code Online (Sandbox Code Playgroud)

的问题是:所述Entry包装物被视为无法比拟的类型到原来的注册表对象consructed内行动

object problem {

  case class Item(name: String)

  val items = new Registry[Item]("elems")

  import testOps._

  implicit val storage: TestStorage[Item] = 
    scala.collection.mutable.Map[Long, Item](
      1L -> Item("whatever")
    )
  /** Compilation error: 
   found   : _1.self.Entry where val _1: testOps.RegistryOps[problem.Item]
   required: eta$0$1.self.Entry
  */
  items.getById(1).foreach(items.remove)
}
Run Code Online (Sandbox Code Playgroud)

问题是:有没有一种方法可以声明Ops签名,以使编译器了解我们正在使用相同的内部类型?(我也试过self.type#EntryRegistryOps没有运气),如果我错过了一些了解,他们实际上是不同的类型,所以应该把它们看做同样可以打破类型的系统我将不胜感激任何解释和例子。谢谢!

Tra*_*own 4

首先,值得注意的是,这里的隐含性并不是真正的问题\xe2\x80\x94如果你写出类似下面的内容,它也会以完全相同的方式失败:

\n\n
new RegistryOps(items).getById(1).foreach(e => new RegistryOps(items).remove(e))\n
Run Code Online (Sandbox Code Playgroud)\n\n

有很多方法可以做你想做的事情,但它们并不令人愉快。一种方法是对隐式类进行脱糖处理,以便您可以让它捕获注册表值的更具体的类型:

\n\n
class Registry[T](name: String) {\n  case class Entry(id: Long, value: T)\n}\n\nobject testOps {\n  import scala.collection.mutable\n\n  type TestStorage[T] = mutable.Map[Long, T]\n\n  class RegistryOps[T, R <: Registry[T]](val self: R)(\n    implicit storage: TestStorage[T]\n  ) {\n    def getById(id: Long): Option[R#Entry] = \n      storage.get(id).map(self.Entry(id, _))\n\n    def remove(entry: R#Entry): Unit = storage - entry.id\n  }\n\n  implicit def toRegistryOps[T](s: Registry[T])(\n    implicit storage: TestStorage[T]\n  ): RegistryOps[T, s.type] = new RegistryOps[T, s.type](s)\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

这工作得很好,无论是在你正在使用它的形式,还是稍微更明确:

\n\n
scala> import testOps._\nimport testOps._\n\nscala> case class Item(name: String)\ndefined class Item\n\nscala> val items = new Registry[Item]("elems")\nitems: Registry[Item] = Registry@69c1ea07\n\nscala> implicit val storage: TestStorage[Item] = \n     |     scala.collection.mutable.Map[Long, Item](\n     |       1L -> Item("whatever")\n     |     )\nstorage: testOps.TestStorage[Item] = Map(1 -> Item(whatever))\n\nscala> val resultFor1 = items.getById(1)\nresultFor1: Option[items.Entry] = Some(Entry(1,Item(whatever)))\n\nscala> resultFor1.foreach(items.remove)\n
Run Code Online (Sandbox Code Playgroud)\n\n

请注意,推断的静态类型resultFor1正是您所期望和想要的。与上面评论中提出的解决方案不同Registry[T]#Entry,这种方法将禁止您从一个注册表中获取条目并从另一个注册表中删除具有相同T. 想必您Entry专门创建了一个内部案例类,因为您想避免这种事情。如果你不在乎,你真的应该用Entry自己的T.

\n\n

附带说明一下,您可能认为只需编写以下内容就可以了:

\n\n
implicit class RegistryOps[T, R <: Registry[T]](val self: R)(\n  implicit storage: TestStorage[T]\n) {\n  def getById(id: Long): Option[R#Entry] = \n    storage.get(id).map(self.Entry(id, _))\n\n  def remove(entry: R#Entry): Unit = storage - entry.id\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

但你错了,因为编译器在对隐式类脱糖时将生成的合成隐式转换方法将使用R比你需要的更宽的范围,并且你将回到与没有R. 所以你必须自己编写toRegistryOps并指定s.type

\n\n

(作为脚注,我不得不说,隐式传递一些可变状态听起来绝对是一场噩梦,我强烈建议不要像在这里所做的那样远程执行任何操作。)

\n