scala中的类型擦除的问题

2024-08-24 10:58
文章标签 类型 问题 scala 擦除

本文主要是介绍scala中的类型擦除的问题,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Overcoming type erasure in Scala

原文来自Overcoming type erasure in Scala。

本文旨在展示一些技术来解决由Scala泛型编程中的类型擦除引起的一些常见问题。

介绍

Scala有一个非常强大的类型系统,Scala是强类型语言。存在类型,结构类型,嵌套类型,路径依赖类型,抽象和具体类型成员,类型边界((upper, lower, view, context),使用站点和声明站点类型方差,支持类型多态(subtype, parametric, F-bounded, ad-hoc),更高级的类型,广义类型的约束……而且这个名单还在继续。

但是,即使Scala的类型系统在理论上非常强大,实际上一些与类型相关的特性由于其运行时环境的限制而受到了削弱 - 这就是类型擦除。

什么是类型擦除?简而言之,这是由Java和Scala编译器执行的一个过程,它在编译后删除所有的泛型类型信息。这意味着我们无法在运行时区分List [Int]List [String]。为什么编译器会这样做?那么,因为Java虚拟机(运行Java和Scala的底层运行时环境)并不知道泛型。

类型擦除存在的历史原因。 Java从一开始就不支持泛型。所以当他们最终加入到Java 5中时,他们不得不保持向后兼容性。他们希望允许与旧的非通用遗留代码的无缝接口(这就是为什么我们有Java中的原始类型)。发生了什么是通用类中的类型参数被替换为Object或其上限。例如:

class Foo[T] {val foo: T
}
class Bar[T <: Something] {val bar: T
}//-----type erasureclass Foo {val foo: Object
}
class Bar {val bar: Something
}

所以,运行时我们是不知道泛型类参数化的实际类。在我们的例子中,编译器只能看到原始的Foo和Bar。

不要认为类型擦除是某人无能或无知的产物。这不是坏的设计,而是一种权衡。

我想谈的是我们如何处理Scala中的类型擦除。不幸的是,没有办法防止类型擦除本身,但是我们会看到一些方法来解决它。

它是如何工作的(或不工作)

这里有一个简单的擦除类型的例子:

object Extractor {def extract[T](list: List[Any]) = list.flatMap {case element: T => Some(element)case _ => None}
}val list = List(1, "string1", List(), "string2")
val result = Extractor.extract[String](list)
println(result) // List(1, string1, List(), string2)

方法extract()获取各种对象的列表,,因为它拥有Any类型的对象,我们可以把数字、布尔值、字符串、其他对象放入其中。顺便说一句,在一段代码中看到List [Any]应该是一个即时的“代码味道”。

所以,我们的愿望是有一个方法,只需要一个混合对象的列表,并只提取某种类型的对象。我们可以通过参数化方法extract()来选择这个类型。在给定的例子中,所选择的类型是String,这意味着我们将尝试从给定列表中提取所有字符串。

从严格的语言角度(没有进入运行时细节),这个代码是合理的。我们知道,模式匹配能够通过解构给定对象的类型而没有问题。但是,由于在JVM上执行的程序,所有通用类型在编译之后被擦除。因此模式匹配不能真正走得太远;类型的“第一级”之外的所有东西都被删除了。直接在Int或String(或任何非泛型类型,如MyNonGenericClass)上匹配我们的变量可以正常工作,但是在T上匹配它(T是泛型参数)则就不能通过编译。编译器会给我们一个警告,说“abstract type pattern T is unchecked since it is eliminated by erasure”。

为了对这些情况提供一些帮助,Scala在2.7版本左右的地方引入了Manifests。 然而,他们有问题,不能代表某些类型,所以Scala 2.10中,他们放弃了它,并使用更强大的TypeTag。

类型标签分为三种不同的类型:

  • TypeTag
  • ClassTag
  • WeakTypeTag

即使这是文档中的官方分类,我认为更好的分类将是这样的:

  • TypeTag:
    • “classic”
    • WeakTypeTag
  • ClassTag

我的意思是,TypeTag和WeakTypeTag实际上是两个相同的事物,只有一个显着的差异(如我们稍后会显示),而ClassTag是一个完全不同的构造。

ClassTag

让我们回到我们的提取器例子,看看我们如何解决类型擦除问题。 我们现在要做的就是向extract()方法添加一个隐式参数:

import scala.reflect.ClassTag
object Extractor {def extract[T](list: List[Any])(implicit tag: ClassTag[T]) =list.flatMap {case element: T => Some(element)case _ => None}
}
val list: List[Any] = List(1, "string1", List(), "string2")
val result = Extractor.extract[String](list)
println(result) // List(string1, string2)

打印语句显示List(string1,string2)

请注意,我们也可以在这里使用上下文绑定语法:

// def extract[T](list: List[Any])(implicit tag: ClassTag[T]) =
def extract[T : ClassTag](list: List[Any]) =

我将使用标准语法来简化代码,不需要额外的语法糖。

那么它是怎样工作的?那么,当我们需要一个类型为ClassTag的隐式值时,编译器会为我们创建这个值。文档说:

If an implicit value of type u.ClassTag[T] is required, the compiler will make one up on demand.
如果需要一个类型为u.ClassTag [T]的隐式值,编译器会根据需要创建一个。

所以,编译器很乐意为我们提供一个需要ClassTag的隐式实例。这种机制也将与TypeTagWeakTypeTag一起使用。

我们在extract()方法中提供了隐式的ClassTag值。一旦我们进入方法体内部会发生什么?

再次看一下这个例子 - 编译器不仅自动为我们提供了隐式参数标记的值,而且我们也不需要使用参数本身。我们从来不需要对Tag值做任何事情。只是因为它存在,我们的模式匹配就能够成功匹配我们列表中的字符串元素。

我们可以检查文档以寻找解释。事实上,它隐藏在这里:

Compiler tries to turn unchecked type tests in pattern matches into checked ones by wrapping a (: T) type pattern as ct(: T), where ct is the ClassTag[T] instance.
编译器试图通过包装一个(_:T)类型模式为ct(_:T),其中ct是ClassTag [T]实例,将模式匹配中未经检查的类型测试变成已检查的类型。

基本上,如果我们为编译器提供一个隐式的ClassTag,它会重写模式匹配中的条件,以使用给定的标签作为extractor。我们的条件:

{case element: T => Some(element)}

由编译器翻译(如果在范围内有一个隐含的标签)到这里:

{case (element @ tag(_: T)) => Some(element)}

如果你以前从未见过“@”构造,那只是给你匹配的类命名的一种方法,例如:

{case Foo(p, q) => // we can only reference parameters via p and qcase f @ Foo(p, q) => // we can reference the whole object via f
}

如果没有可用的类型为T的隐式ClassTag,则编译器将被削弱(由于缺少类型信息),并且会发出警告,表明我们的模式匹配将受到类型T上的类型擦除的损害。编译不会中断,但是当我们进行模式匹配时,不要期望编译器知道什么是T(因为它将在运行时被JVM擦除)。如果我们为类型T提供了一个隐式的ClassTag,那么编译器会很高兴在编译时提供一个合适的ClassTag,就像我们在例子中看到的那样。标签将带来关于T是一个字符串的信息,类型删除不能触摸它。

但是有一个重要的弱点。如果我们想要在更高级别上区分我们的类型,并从我们的初始列表中获得List [Int]的值,而忽略例如列出[String],我们不能这样做:

val list: List[List[Any]] = List(List(1, 2), List("a", "b"))
val result = Extractor.extract[List[Int]](list)
println(result) // List(List(1, 2), List(a, b))

我们只想提取List [Int],但是我们也得到了List [String]Class tags不能在更高层次上进行区分。

这意味着我们的提取器可以区分例如setslists,但它不能将一个列表与另一个列表区分开来(例如List [Int]List [String])。当然,这不仅仅是对于列表,这适用于所有的通用trait/class。

TypeTag

ClassTag失败的地方,开发人员用TypeTag来弥补。 它可以区分List [String]List [Integer]。 它也可以更深入一些,比如区分List [Set [String]]中的List [Set [Int]]。因为TypeTag在运行时有更丰富的关于泛型类型的信息。

我们可以很容易地得到所讨论类型的完整路径以及所有嵌套类型(如果有的话)。 要得到这个信息,你只需要在给定的标签上调用tpe()。

这是一个例子。 隐式标签参数由编译器提供,就像ClassTag一样。 请注意“args”参数 - 它是包含ClassTag没有的其他类型信息的信息(有关由Int参数化的List的信息)。

import scala.reflect.runtime.universe._
object Recognizer {def recognize[T](x: T)(implicit tag: TypeTag[T]): String =tag.tpe match {case TypeRef(utype, usymbol, args) =>List(utype, usymbol, args).mkString("\n")}
}val list: List[Int] = List(1, 2)
val result = Recognizer.recognize(list)
println(result)
// prints:
//   scala.type
//   type List
//   List(Int)

我在这里介绍了一个新的对象 - 一个Recognizer。

不幸的是,我们无法使用TypeTags实现Extractor。但是我们可以获得更多关于类型的信息,比如了解更高类型(也就是说,能够区分List[X]List[Y]),但是它们的缺点是它们不能用于运行。

我们可以使用TypeTag在运行时获取某种类型的信息,但是我们不能用它来在运行时找出某个对象的类型。我们传入recognize()的是一个简单的List [Int];这是我们的List(1,2)值的声明类型。但是,如果我们将List(1,2)声明为List [Any],TypeTag会告诉我们我们已经通过一个List [Any]

下面是ClassTags和TypeTag之间的两个主要区别:

  1. ClassTag不知道“更高类型”;给定一个List [T],一个ClassTag只知道这个值是一个List,对T一无所知。
  2. TypeTag知道“更高类型”,并且有更丰富的类型信息,但不能用于在运行时获取有关值的类型信息。换句话说,TypeTag提供了关于类型的运行时信息,而ClassTag提供了关于该值的运行时信息(更具体地说,是在运行时告诉我们所讨论的值的实际类型的信息)。

还有一点值得一提的是ClassTag和(Weak)TypeTag之间的区别:ClassTag是一个经典的老式类。它为每个类型捆绑了一个单独的实现,这使得它成为一个标准的类型模式。另一方面,(Weak)TypeTag有点复杂,为了使用它,我们需要在代码中有一个特殊的导入,正如你在前面给出的代码片段中注意到的那样。我们需要导入universe:

Universe provides a complete set of reflection operations which make it possible for one to reflectively inspect Scala type relations, such as membership or subtyping.
Universe提供了一套完整的反射操作,使得人们可以反思性地检查Scala类型关系,例如成员资格或子类型。

不要担心,只需要导入正确的Universe,并且在(Weak)TypeTag(scala.reflect.runtime.universe._ (docs))的情况下。

WeakTypeTag

您可能觉得TypeTag和WeakTypeTag是非常相似的,因为迄今为止所有的差异都是在ClassTag中解释的。 这是正确的; 他们确实是同一个工具的两个变种。 但是,有一个重要的区别。

我们看到TypeTag足够聪明,可以检查类型,类型参数,类型参数等等。但是,所有类型都是具体的。 如果一个类型是抽象的,TypeTag将无法解决它。 这是WeakTypeTag进场的地方。 让我们来修改TypeTag示例一下:

val list: List[Int] = List(1, 2)
val result = Recognizer.recognize(list)

看那边的那个Int?它可以是任何其他具体类型,如StringSet [Double]MyCustomClass。但是如果你有一个抽象类型,你需要一个WeakTypeTag

这是一个例子。 请注意,我们需要对抽象类型的引用,所以我们只需将所有内容都包含在抽象类中。

import scala.reflect.runtime.universe._
abstract class SomeClass[T] {object Recognizer {def recognize[T](x: T)(implicit tag: WeakTypeTag[T]): String =tag.tpe match {case TypeRef(utype, usymbol, args) =>List(utype, usymbol, args).mkString("\n")}}val list: List[T]val result = Recognizer.recognize(list)println(result)
}new SomeClass[Int] { val list = List(1) }
// prints:
//   scala.type
//   type List
//   List(T)

结果类型是一个List [T]

如果我们使用TypeTag而不是WeakTypeTag,编译器会抱怨“no TypeTag available for List[T]”。 所以,你可以把WeakTypeTag看作TypeTag的一个超集。

请注意,WeakTypeTag尽可能具体,所以如果有一个类型标签可用于某种抽象类型,WeakTypeTag将使用该类型标记,从而使类型具体而不是抽象的。

结论

在我们完成之前,让我提一下,每个类型标签也可以使用可用的助手来显式实例化:

import scala.reflect.classTag
import scala.reflect.runtime.universe._val ct = classTag[String]
val tt = typeTag[List[Int]]
val wtt = weakTypeTag[List[Int]]val array = ct.newArray(3)
array.update(2, "Third")println(array.mkString(","))
println(tt.tpe)
println(wtt.equals(tt))//  prints:
//    null,null,Third
//    List[Int]
//    true

就这样。 我们看到了三个构造,ClassTag,TypeTag和WeakTypeTag,它们将帮助您在日常Scala生活中解决大部分类型的擦除问题。

请注意,使用标签(这基本上是反射下)可以减慢速度,使生成的代码显着变大,所以不要在你的库中添加隐式类型标签,以使编译器更加“智能” 没有实际的原因。 保存它们,当你真的需要它们。

而当你需要它们的时候,它们将会提供一个强大的武器来对付JVM的类型擦除。

这篇关于scala中的类型擦除的问题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1102291

相关文章

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

Spring Boot 配置文件之类型、加载顺序与最佳实践记录

《SpringBoot配置文件之类型、加载顺序与最佳实践记录》SpringBoot的配置文件是灵活且强大的工具,通过合理的配置管理,可以让应用开发和部署更加高效,无论是简单的属性配置,还是复杂... 目录Spring Boot 配置文件详解一、Spring Boot 配置文件类型1.1 applicatio

SpringBoot启动报错的11个高频问题排查与解决终极指南

《SpringBoot启动报错的11个高频问题排查与解决终极指南》这篇文章主要为大家详细介绍了SpringBoot启动报错的11个高频问题的排查与解决,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一... 目录1. 依赖冲突:NoSuchMethodError 的终极解法2. Bean注入失败:No qu

MySQL新增字段后Java实体未更新的潜在问题与解决方案

《MySQL新增字段后Java实体未更新的潜在问题与解决方案》在Java+MySQL的开发中,我们通常使用ORM框架来映射数据库表与Java对象,但有时候,数据库表结构变更(如新增字段)后,开发人员可... 目录引言1. 问题背景:数据库与 Java 实体不同步1.1 常见场景1.2 示例代码2. 不同操作

如何解决mysql出现Incorrect string value for column ‘表项‘ at row 1错误问题

《如何解决mysql出现Incorrectstringvalueforcolumn‘表项‘atrow1错误问题》:本文主要介绍如何解决mysql出现Incorrectstringv... 目录mysql出现Incorrect string value for column ‘表项‘ at row 1错误报错

如何解决Spring MVC中响应乱码问题

《如何解决SpringMVC中响应乱码问题》:本文主要介绍如何解决SpringMVC中响应乱码问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring MVC最新响应中乱码解决方式以前的解决办法这是比较通用的一种方法总结Spring MVC最新响应中乱码解

pip无法安装osgeo失败的问题解决

《pip无法安装osgeo失败的问题解决》本文主要介绍了pip无法安装osgeo失败的问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 进入官方提供的扩展包下载网站寻找版本适配的whl文件注意:要选择cp(python版本)和你py

解决Java中基于GeoTools的Shapefile读取乱码的问题

《解决Java中基于GeoTools的Shapefile读取乱码的问题》本文主要讨论了在使用Java编程语言进行地理信息数据解析时遇到的Shapefile属性信息乱码问题,以及根据不同的编码设置进行属... 目录前言1、Shapefile属性字段编码的情况:一、Shp文件常见的字符集编码1、System编码

Spring MVC使用视图解析的问题解读

《SpringMVC使用视图解析的问题解读》:本文主要介绍SpringMVC使用视图解析的问题解读,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring MVC使用视图解析1. 会使用视图解析的情况2. 不会使用视图解析的情况总结Spring MVC使用视图

Redis解决缓存击穿问题的两种方法

《Redis解决缓存击穿问题的两种方法》缓存击穿问题也叫热点Key问题,就是⼀个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击,本文给大家介绍了Re... 目录引言解决办法互斥锁(强一致,性能差)逻辑过期(高可用,性能优)设计逻辑过期时间引言缓存击穿:给