Scala的ClassTag如何为?

摘要:本文通过一个经典的 Map[String, Any] 类型转换场景,深入探讨 Scala 中 ClassTag 的必要性。文章通过三个迭代方案,演示了如何从脆弱的 asInstanceOf 逐步进化到利用 ClassTag 绕过 JVM 类
Scala官方文档中对于ClassTag的定义如下: ClassTag[T]保存着在运行时被JVM擦除的类型T的信息。当我们在运行时想获得被实例化的Array的类型信息的时候,这个特性会比较有用。 下面请看一个具体的场景: 场景 假定有一个Map[String, Any],给定一个指定的key,我们需要检查Map中是否存在该key对应的value,如果存在,则优雅地返回这个值。看起来很简单,下面让我们来实现它,并且逐渐理解ClassTag。 解决方案一 我们的第一次尝试如下所示: def main(args: Array[String]): Unit = { class Animal val myMap: collection.Map[String, Any] = Map("Number" -> 1, "Greeting" -> "Hello World", "Animal" -> new Animal) /* 下面注释的代码将会不通过编译 * Any不能被当时Int使用 */ //val number:Int = myMap("Number") //println("number is " + number) //使用类型转换,可以通过编译 val number: Int = myMap("Number").asInstanceOf[Int] println("number is " + number) //下面的代码将会抛出ClassCastException val greeting: String = myMap("Number").asInstanceOf[String] } 运行结果如下: number is 1 Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String at learnscala.Test$.main(Test.scala:25) at learnscala.Test.main(Test.scala) 上面的代码有几个很显然的问题: 首先,当我们把Any直接当成Int来使用的时候,编译器是不会通过的: // 这将不会通过编译 val number:Int = myMap("Number") 编译器看起来没有什么错误,但是问题在于,我们没有办法使用Map中值的具体类型。换句话说,如果我们只是把值的类型设置成Any我们没办法受益于Scala提供的类型系统,所以我们需要修改代码。 为了通过编译,我们使用了如下的类型转换把获取的值变成了Int类型: val number:Int = myMap("Number").asInstanceOf[Int] 这种做法虽然能有效果,但是当我们尝试转换一个不相关的类型的时候,asInstanceOf会抛出一个ClassCastException异常。 所以下面这行代码,会在运行时抛出异常,因为我们试图转换一个Int值为String。 val greeting:String = myMap("Number").asInstanceOf[String] 我们又引入了一个新的问题。 就算我们使用Map的get()方法,编译器还是不会通过,因为Option[Any]也没有办法当成Option[Int]来使用: //下面的代码将不会通过编译 val number:Option[Int] = myMap.get("Number") 你可能会想什么谁的代码里面会使用Map[String, Any],这显然不是一个好的设计。但是我们先忽略这一点,并假设这个结构确实存在,下面我们回到这个问题。 使用asInstanceOf显然也不是一个好的方案,处理ClassCastException的办法之一是使用try/catch,但是这个方案并不是一个可靠并且优雅的方案,所以我们并不会采用。
阅读全文