【问题标题】:Type erasure for memoized/cached requests in ScalaScala中记忆/缓存请求的类型擦除
【发布时间】:2021-02-11 09:11:18
【问题描述】:

我在 Scala 中收到类型擦除警告。

问题是我需要缓存传出请求。尽管由于当前设置的方式,请求可以包装不同的返回类型。

我尝试通过在 getOrPut 方法中添加类型参数来解决它。但是在 match 语句中,由于类型擦除,包装在 Future 中的任何内容都不会被检查。

我可以通过使用@unchecked 来消除类型擦除警告,但我想知道是否有更好的方法来确保返回的类型是所需的类型。

简化示例:

class RequestCache() {
  val underlying: scala.collection.mutable.Map[String, Future[Any] =
    scala.collection.mutable.Map()

  def getOrPut[A](
    key: String,
    val: Future[Request[A]]
  ): Future[Request[A]] = {
    underlying.get(key) match {
      case None => { 
        underlying.update(key, val)
        val
      }
      case Some(storedVal: Future[Request[A]]) => storedVal
    }
  }
    
}

【问题讨论】:

    标签: scala types type-erasure


    【解决方案1】:

    看来您的 Map 值将属于 Future[Request[A]]] 类型。为什么不让 class RequestCache 带一个类型参数,这种方法不会出现类型擦除问题:

    class RequestCache[A] {
      val underlying: scala.collection.mutable.Map[String, Future[Request[A]]] =
        scala.collection.mutable.Map()
    
      def getOrPut(key: String, value: Future[Request[A]]): Future[Request[A]] =
        underlying.get(key) match {
          case None =>
            underlying.update(key, value)
            value
          case Some(storedVal: Future[Request[A]]) =>
            storedVal
        }
    }
    

    【讨论】:

    • 这实际上是一个问题,因为它需要多个缓存实例,每个 [A] 一个实例,但是在这种情况下我们希望使用一个实例运行它。
    【解决方案2】:

    这无法完全解决,因为您要放入所有可能类型的Future,因此一旦您忘记了确切类型一次,就永远无法 100% 编译时保证您可以恢复它。您必须自己转换类型并希望它匹配。

    我看到了解决这个问题的两种方法:

    • 在密钥中编码类型(不要忘记在密钥中!),以便您知道可以安全地转换为哪种类型
    • 使用已擦除类型获取结果并尝试强制转换并处理可能的失败

    第一个可以完成,例如像这样:

    trait CacheKey {
      type Out
    }
    object CacheKey {
      type Aux[O] = CacheKey { type Out = O }
    }
    
    case object GlobalSetting extends CacheKey { type Out = String }
    case class UserSetting(userID: UUID) extends CacheKey { type Out = Int }
    
    class Cache() {
      private val underlying =
        scala.collection.mutable.Map.empty[CacheKey, Future[_]]
    
      def getOrPut[O](
        key: CacheKey.Aux[O],
        value: => Future[O] // should be by-name!
      ): Future[O] = underlying.get(key) match {
        case Some(storedVal) =>
          storedVal.asInstanceOf[Future[O]]
        case None =>
          underlying.update(key, val)
          value
      }
    }
    

    使用String,您可以尝试这样做:

    cache.getOrPut("foo", intFuture)
    cache.getOrPut("foo", stringFuture)
    

    第一个调用会设置缓存,第二个调用会返回一个结果类型无效的 Future,所以如果你只是强制转换,你会在 Future 的某个随机点收到ClassCastException

    另一种方法是始终获取结果,然后检查它是否有效(这很难做到防弹):

    class Cache() {
      private val underlying =
        scala.collection.mutable.Map.empty[CacheKey, (ClassTag[_], Future[_])]
    
      def getOrPut[O: ClassTag](
        key: String,
        value: => Future[O] // should be by-name!
      ): Future[O] = underlying.get(key) match {
        case Some((originalTag, storedVal)) =>
          if (classTag[O] == originalTag)
            storedVal.asInstanceOf[Future[O]]
          else
            throw new Exception("Mismatched cache types")
        case None =>
          underlying.update(key, val)
          value
      }
    }
    

    这种方法的问题是ClassTag 没有区分具有不同类型参数的泛型,例如classTag[List[Int]] == classTag[List[String]] 返回 true。但是,对于任何基于运行时反射的解决方案,无论您选择什么,都会遇到这个或类似的问题。

    使用编译时反射,您可以完全避免该问题,但在任何时候您都必须知道密钥的类型,因此您不能只传递CacheKey - 因为那样您只会知道结果是Future[key.Out] 类型 - 您必须将 CacheKey.Aux[O]O 作为类型参数一起传递。视情况而定,这可能是非问题或交易破坏者。

    顺便说一句,您应该在此处为 Future 使用别名参数 - Future 在您创建它的那一刻开始评估,因此使用您的语法 Scala 将开始评估您的 Future 然后它会检查缓存如果在同一个键下还有一些其他的未来。如果它更早地开始和结束,可能会有一些好处,但你可能仍然会遇到一个错误的逻辑,因为新的Future 将继续执行。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-05-28
      • 2020-07-27
      • 2012-08-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多