【问题标题】:Working around a type erasure match in Scala在 Scala 中解决类型擦除匹配问题
【发布时间】:2018-01-30 04:57:07
【问题描述】:

这是示例代码:

object GenericsTest extends App {
  sealed trait CommonResponse
  trait Requester[SomeType] {
    trait Response extends CommonResponse {
      def entity: SomeType
    }

    case class SomeResponse(entity: SomeType) extends Response
    // More Responses here...
    def getResponse(): Response
  }

  object StringRequester extends Requester[String] {
    override def getResponse(): StringRequester.Response = SomeResponse("somestring")
  }

  object IntegerRequester extends Requester[Integer] {
    override def getResponse(): IntegerRequester.Response = SomeResponse(42)
  }

  val response: CommonResponse = IntegerRequester.getResponse()
  response match {
    case r: StringRequester.Response => println(s"Got string response ${r.entity}") // hits here =(
    case r: IntegerRequester.Response => println(s"Got integer response ${r.entity}")
    case other => println(s"Got other $other")
  }
}

它打印 "Got string response 42" 而不是 "Got integer response 42"

真正的代码是一个实现不同类型的索引的服务,如果数据已经被索引或者没有被索引等等,会返回不同的Responses。

  1. 有什么解决方法吗?
  2. 如何让编译器对这种特殊情况发出警告?

【问题讨论】:

  • -未选中以警告外部指针。
  • 如果您使用的是 scala >= 2.12.0,只需将 trait Response 替换为 abstract class Response 即可解决问题
  • 很好,@OlegPyzhcov!我认为你找到了最好的解决方案。你愿意把它作为答案吗?

标签: scala


【解决方案1】:

您可以在响应中公开对外部Requester 的引用:

trait Requester[SomeType] {
  trait Response {
    def requester: Requester.this.type = Requester.this
    // ...
  }
  // ...
}

然后你可以在response.requester上匹配:

response.requester match {
  case StringRequester => println(s"Got string response ${response.entity}") // hits here =(
  case IntegerRequester => println(s"Got integer response ${response.entity}")
  case _ => println(s"Got other $response")
}

【讨论】:

    【解决方案2】:

    由于inner trait Responseruntime 中没有实现outer 类型检查的outer 方法,这是导致trait 实际上是 Java

    中的interface

    所以对于您的示例,您应该使用SomeResponse 进行类型匹配,例如:

      response match {
        case r: StringRequester.SomeResponse => println(s"Got string response ${r.entity}") // hits here =(
        case r: IntegerRequester.SomeResponse => println(s"Got integer response ${r.entity}")
        case _ => println(s"Got other")
      }
    

    SomeResponse 拥有outer 方法,用于在运行时进行类型检查。见:

      public Test$Requester Test$Requester$SomeResponse$$$outer();
        Code:
           0: aload_0
           1: getfield      #96                 // Field $outer:LTest$Requester;
           4: areturn
    

    【讨论】:

      【解决方案3】:

      Scala 在运行时擦除泛型类型。 例如,List[String] 和 List[Integer] 的运行时类型是相同的。因此,您的代码不起作用。

      例如,

        sealed trait Foo
        case class Bar[T](value: T) extends Foo
      
        val f1: Foo = Bar[String]("Apple")
        val f2: Foo = Bar[Integer](12)
      
        //Will print ---A even type of f2 is Integer. 
        f2 match {
          case x: Bar[String]=> println("--- A")
          case x: Bar[Integer] => println("--- B")
          case _ => println("---")
        }
      

      上面将打印---A,因为在scala中,泛型类型在运行时被擦除,因此,编译器不会知道f2的类型。

      在您的情况下,您已经定义了Requester 的 2 个实例。 StringRequesterIntegerRequesterStringRequest的响应typeRequester[String]#ResponseIntegerRequesterRequester[String]#Response。 这里,Response 是一个路径依赖类型,即不同实例的响应类型不同。 例如,StringRequester1.Response 不等于 StringRequester2.Response

      但是,由于泛型,上述条件会失败。因为,由于泛型中的类型擦除,SomeType 类型在运行时从 Requester 中删除。

      i.e. Requester[String]#Response will be Requester[_]#Response
      and Requester[Integer]#Response will be Requester[_]#Response
      
      StringRequester.getResponse().isInstanceOf[IntegerRequester.Response] //will print true.
      //because both type of StringRequester.getResponse() and IntegerRequest.Response is Requester[_]#Response.
      

      因此,两种类型都是相等的。因此,您的代码无法给出正确的结果。

        response match {
          case r: StringRequester.Response => println(s"Got string response ${r.entity}") // hits here =(
          case r: IntegerRequester.Response => println(s"Got integer response ${r.entity}")
          case other => println(s"Got other $other")
        }
      

      在上面的代码中,r 的两种情况类型在运行时都是Requester[_]#Response,因此两者都会匹配,Scala 匹配第一个找到的情况,即StringRequester.Response。如果你如下交换位置,它将打印整数。

        response match {
          case r: IntegerRequester.Response => println(s"Got integer response ${r.entity}")
          case r: StringRequester.Response => println(s"Got string response ${r.entity}") // hits here =(
          case other => println(s"Got other $other")
        }
      

      以下是解决方法: 您可以使用反射类型检查,如下所示。

        sealed trait Foo
        case class Bar[T : TypeTag](value: T) extends Foo {
          def typeCheck[U: TypeTag] = typeOf[T] =:= typeOf[U]
        }
      
        val f1:Foo = Bar[String]("apple")
        val f2:Foo = Bar[Integer](12)
      
        // Will print Integer type.
        f2 match {
          case x: Bar[String] if x.typeCheck[String] => println("Value is string type.")
          case x: Bar[Integer] if x.typeCheck[Integer] => println("Value is Integer type.")
          case _ => println("--- none ---")
        }
      

      【讨论】:

        【解决方案4】:

        正如我在评论中所说,用abstract class Response 替换trait Response 解决了Scala 2.12 中的问题。这将导致捕获$outer 指针并在模式匹配中对其进行检查(您可以使用-Xprint:jvm 编译器参数查看它。

        我无法找到release notes for 2.12.0 中指定的此更改,因此可能不是故意的。强烈建议用单元测试覆盖它。

        【讨论】:

          猜你喜欢
          • 2011-11-04
          • 2015-08-18
          • 1970-01-01
          • 1970-01-01
          • 2013-09-07
          • 1970-01-01
          • 1970-01-01
          • 2018-04-12
          • 2015-10-06
          相关资源
          最近更新 更多