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

相关文章

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

购买磨轮平衡机时应该注意什么问题和技巧

在购买磨轮平衡机时,您应该注意以下几个关键点: 平衡精度 平衡精度是衡量平衡机性能的核心指标,直接影响到不平衡量的检测与校准的准确性,从而决定磨轮的振动和噪声水平。高精度的平衡机能显著减少振动和噪声,提高磨削加工的精度。 转速范围 宽广的转速范围意味着平衡机能够处理更多种类的磨轮,适应不同的工作条件和规格要求。 振动监测能力 振动监测能力是评估平衡机性能的重要因素。通过传感器实时监

缓存雪崩问题

缓存雪崩是缓存中大量key失效后当高并发到来时导致大量请求到数据库,瞬间耗尽数据库资源,导致数据库无法使用。 解决方案: 1、使用锁进行控制 2、对同一类型信息的key设置不同的过期时间 3、缓存预热 1. 什么是缓存雪崩 缓存雪崩是指在短时间内,大量缓存数据同时失效,导致所有请求直接涌向数据库,瞬间增加数据库的负载压力,可能导致数据库性能下降甚至崩溃。这种情况往往发生在缓存中大量 k

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s

【编程底层思考】垃圾收集机制,GC算法,垃圾收集器类型概述

Java的垃圾收集(Garbage Collection,GC)机制是Java语言的一大特色,它负责自动管理内存的回收,释放不再使用的对象所占用的内存。以下是对Java垃圾收集机制的详细介绍: 一、垃圾收集机制概述: 对象存活判断:垃圾收集器定期检查堆内存中的对象,判断哪些对象是“垃圾”,即不再被任何引用链直接或间接引用的对象。内存回收:将判断为垃圾的对象占用的内存进行回收,以便重新使用。

flume系列之:查看flume系统日志、查看统计flume日志类型、查看flume日志

遍历指定目录下多个文件查找指定内容 服务器系统日志会记录flume相关日志 cat /var/log/messages |grep -i oom 查找系统日志中关于flume的指定日志 import osdef search_string_in_files(directory, search_string):count = 0

【VUE】跨域问题的概念,以及解决方法。

目录 1.跨域概念 2.解决方法 2.1 配置网络请求代理 2.2 使用@CrossOrigin 注解 2.3 通过配置文件实现跨域 2.4 添加 CorsWebFilter 来解决跨域问题 1.跨域概念 跨域问题是由于浏览器实施了同源策略,该策略要求请求的域名、协议和端口必须与提供资源的服务相同。如果不相同,则需要服务器显式地允许这种跨域请求。一般在springbo