如何在Scala中对泛型类型进行模式匹配?
如何在Scala中对泛型类型进行模式匹配?
假设我们有一个通用类Container
:
case class Container[+A](value: A)
然后我们想要模式匹配一个包含Double
和Any
类型的Container
:
val double = Container(3.3) var container: Container[Any] = double
为了做到这一点,我们通常会这样写:
container match { case c: Container[String] => println(c.value.toUpperCase) case c: Container[Double] => println(math.sqrt(c.value)) case _ => println("_") }
然而,编译器会给出两个警告,分别对应前两个case。例如,第一个警告说:“类型模式Container[String]中的非变量类型参数String未经检查,因为它被擦除。”由于擦除的存在,运行时无法区分不同类型的容器,因此第一个case将被匹配。结果,类型为Container[Double]
的容器将被第一个case匹配,而该case实际上是用于匹配Container[String]
对象的,因此会在Double
上调用toUpperCase
方法,从而抛出一个java.lang.ClassCastException
异常。
如何匹配特定类型的Container
?
在Scala中,如何对泛型类型进行模式匹配?
问题的出现原因是无法直接对泛型类型进行模式匹配,因为在运行时无法获取泛型类型的具体信息。解决方法是使用isInstanceOf和asInstanceOf方法进行类型检查和类型转换。
下面是一个可能的解决方法,使用isInstanceOf和asInstanceOf方法来进行模式匹配:
container match { case Container(x) if x.isInstanceOf[String] => println(x.asInstanceOf[String].toUpperCase) case Container(x) if x.isInstanceOf[Double] => println(math.sqrt(x.asInstanceOf[Double])) case _ => println("_") }
然而,这种方法看起来并不优雅。Scala的创建者Martin Odersky教授表示,应该避免使用isInstanceOf和asInstanceOf方法。
Rob Norris在Coursera的课程论坛中指出,根据类型进行模式匹配是一种不好的做法:case foo: Bar => ... 。Scala鼓励充分利用静态类型,并避免在运行时进行类型检查。这与Haskell/ML世界的哲学是一致的。case子句应该匹配构造函数,而不是类型。
为了解决Container匹配问题,可以为每种类型定义一个特殊的容器:
class Container[+A](val value: A) case class StringContainer(override val value: String) extends Container(value) case class DoubleContainer(override val value: Double) extends Container(value)
现在将匹配的对象从类型改为构造函数:
container match { case StringContainer(x) => println(x.toUpperCase) case DoubleContainer(x) => println(math.sqrt(x)) case _ => println("_") }
显然,我们也可以在StringContainer和DoubleContainer两个对象中定义unapply方法,并使用相同的匹配方式,而不是继承Container类:
case class Container[+A](val value: A) object StringContainer { def unapply(c: Container[String]): Option[String] = Some(c.value) } object DoubleContainer { def unapply(c: Container[Double]): Option[Double] = Some(c.value) }
然而,由于JVM类型擦除的原因,这种方法无法正常工作。
参考Rob Norris的帖子,可以在这里找到这个答案:https://class.coursera.org/progfun-002/forum/thread?thread_id=842#post-3567。不幸的是,除非你参加了Coursera的课程,否则无法访问。
问题的出现原因是在Scala中,当需要对泛型类型进行模式匹配时,无法直接使用通用的类型参数进行匹配。在上述代码中,原始的解决方法是使用Manifest来对泛型类型进行匹配,但是在最新版本的Scala中,Manifest已经被弃用。
解决方法是使用TypeTag来替代Manifest进行泛型类型的模式匹配。在代码中,首先需要导入reflect.runtime.universe._包,然后在matchContainer方法的定义中添加[A: TypeTag]来指定TypeTag作为类型参数。然后,在模式匹配的每个case语句中,将manifest改为typeOf,并使用typeOf[A]来获取泛型类型的Type对象。然后,可以使用<:<运算符来检查类型的关系,并进行相应的操作。
需要注意的是,在最新版本的Scala中,Manifest已被弃用,推荐使用TypeTag来进行泛型类型的模式匹配。
以下是修改后的代码:
import reflect.runtime.universe._ def matchContainer[A: TypeTag](c: Container[A]) = c match { case c: Container[String] if typeOf[A] <:< typeOf[String] => println("string: " + c.value.toUpperCase) case c: Container[Double] if typeOf[A] <:< typeOf[Double] => println("double: " + math.sqrt(c.value)) case c: Container[_] => println("other") }
问题的原因是在Scala中,当我们需要对泛型类型进行模式匹配时,不能直接使用类型参数,而是需要使用类型擦除后的类型进行匹配。这是因为在运行时,泛型类型的类型参数信息会被擦除,只剩下原始类型。
解决方法是通过使用类型擦除后的类型进行匹配。在给定的示例中,我们可以通过匹配容器中的值的类型来实现。由于容器只包含一个泛型类型的值,我们可以直接匹配该值的类型。
具体的解决方法如下所示:
container match { case Container(x: String) => println("string") case Container(x: Double) => println("double") case _ => println("w00t") }
在这个例子中,我们使用了模式匹配的语法来匹配容器的值的类型。首先,我们匹配了包含字符串的情况,并打印出"string"。然后,我们匹配了包含双精度浮点数的情况,并打印出"double"。最后,我们使用通配符来匹配其他情况,并打印出"w00t"。
这样,我们就可以根据容器中值的类型进行相应的操作。通过使用类型擦除后的类型进行模式匹配,我们可以绕过泛型类型在运行时的类型参数擦除问题,实现对泛型类型的模式匹配。